fix(card): venues include shops; NIP.md: 35679 is an Access Pass (L1/L2)
- L2: a 'venue' can now be any spot (30818) OR shop (30017) you own — myVenues is normalised; the pass 'a' tag uses the venue's real kind; /verify confirms issuer ownership for spots, shops and events. - L1: NIP.md renames kind 35679 from 'Park Access Card' to 'Access Pass' and documents the target binding (spot / shop / event).
This commit is contained in:
parent
04a43a8afa
commit
40e62231b3
3 changed files with 29 additions and 17 deletions
24
NIP.md
24
NIP.md
|
|
@ -1479,11 +1479,16 @@ When a match is found, the app can facilitate P2P trading via encrypted DMs (NIP
|
|||
|
||||
SkateSpots supports physical-print cards that are cryptographically signed on Nostr and verifiable by anyone.
|
||||
|
||||
### Park Access Card (kind 35679)
|
||||
### Access Pass (kind 35679)
|
||||
|
||||
Issued by a private skatepark owner to a specific rider, granting access rights. The park's Nostr key signs the event — anyone can verify the card is genuine by scanning the QR code.
|
||||
A signed credential issued by an owner to a specific holder. The owner's Nostr
|
||||
key signs the event and it is **bound to what it grants access to** via an `a`
|
||||
tag — a **venue spot** (`30818`), a **shop** (`30017`), or an **event** (`31923`,
|
||||
an organiser's check-in pass). Anyone can verify it by scanning the QR.
|
||||
|
||||
**Constraint:** Park Access Cards for private parks (`access=private` in kind 30818) should only be issued by the park's owner pubkey. Clients verify the issuer's pubkey matches the park event's `pubkey` field.
|
||||
**Constraint:** an Access Pass may only be issued by the key that **owns** the
|
||||
target (`a`). Clients verify the issuer's pubkey matches the target event's
|
||||
`pubkey` — a venue/shop owner, or the event organiser.
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -1503,12 +1508,13 @@ Issued by a private skatepark owner to a specific rider, granting access rights.
|
|||
}
|
||||
```
|
||||
|
||||
**Venue binding (`a`) — REQUIRED for new passes:** it points at the park's own
|
||||
spot (kind 30818). A pass is only issuable by the **key that owns that venue**,
|
||||
and the `/verify` page fetches the spot and confirms its **author pubkey equals
|
||||
the pass's signer**. The `/card` studio only offers the Park type when you own a
|
||||
venue, and won't publish without one. (Legacy passes with no `a` still verify as
|
||||
a signature but are shown as **not venue-bound**.)
|
||||
**Target binding (`a`) — REQUIRED for new passes:** it points at what the pass
|
||||
grants access to — a **venue spot** (`30818`), a **shop** (`30017`), or an
|
||||
**event** (`31923`). A pass is only issuable by the **key that owns the target**,
|
||||
and the `/verify` page fetches it and confirms its **author pubkey equals the
|
||||
pass's signer**. The `/card` studio only offers the Park/Event types when you
|
||||
own a venue/event, and won't publish without one. (Legacy passes with no `a`
|
||||
still verify as a signature but are shown as **not bound**.)
|
||||
|
||||
### Card Themes
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { useRiderCard, useRiderRoles, useCardProfile, useIssuedPasses, useRevoke
|
|||
import { useUserCrews } from '@/hooks/useCrews';
|
||||
import { useUserSkaterProfile } from '@/hooks/useSkaters';
|
||||
import { useSkateSpots } from '@/hooks/useSkateSpots';
|
||||
import { useSkateShops } from '@/hooks/useSkateShops';
|
||||
import { useSkateEvents } from '@/hooks/useSkateEvents';
|
||||
import { useMyRsvpCoords, useEventRsvps, eventCoord } from '@/hooks/useEventRsvps';
|
||||
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||
|
|
@ -50,7 +51,7 @@ import { genUserName } from '@/lib/genUserName';
|
|||
import { useTranslation } from '@/contexts/language';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { RIDER_CARD_KIND, RIDER_CARD_D, PARK_ACCESS_CARD_KIND, SKILL_BADGE_KIND, cardVerifyUrl, riderCardTags, type RiderCardConfig } from '@/lib/riderCard';
|
||||
import { SKATE_SPOT_KIND, SKATE_CREW_KIND, CALENDAR_EVENT_KIND, SKATER_STYLES, SKATER_LEVELS } from '@/lib/types';
|
||||
import { SKATE_SPOT_KIND, SKATE_SHOP_KIND, SKATE_CREW_KIND, CALENDAR_EVENT_KIND, SKATER_STYLES, SKATER_LEVELS } from '@/lib/types';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Printer, Loader2, Share2, Download, Check, Copy, Award, Zap, Info, Palette, ChevronUp, ChevronDown, User, Building2, Users, Ticket, MoreHorizontal, Pencil, CheckCircle2, RefreshCw, Upload } from 'lucide-react';
|
||||
|
|
@ -168,6 +169,7 @@ export default function RiderCardPage() {
|
|||
const metadata = cardProfile.data;
|
||||
const { data: savedCard } = useRiderCard(user?.pubkey);
|
||||
const { data: allSpots } = useSkateSpots(200);
|
||||
const { data: allShops } = useSkateShops(200);
|
||||
const { memberships } = useUserCrews(user?.pubkey);
|
||||
const { data: skaterProfile } = useUserSkaterProfile(user?.pubkey);
|
||||
const { data: roles = [] } = useRiderRoles(user?.pubkey);
|
||||
|
|
@ -276,10 +278,14 @@ export default function RiderCardPage() {
|
|||
const eventPasses = activeEvent
|
||||
? issuedPasses.filter((p) => p.coord === `${CALENDAR_EVENT_KIND}:${activeEvent.pubkey}:${activeEvent.identifier}`)
|
||||
: [];
|
||||
const myVenues = useMemo(
|
||||
() => (allSpots ?? []).filter((s) => s.pubkey === user?.pubkey),
|
||||
[allSpots, user?.pubkey],
|
||||
);
|
||||
// A "venue" you can issue a pass for is any spot OR shop you own.
|
||||
const myVenues = useMemo(() => {
|
||||
const spots = (allSpots ?? []).filter((s) => s.pubkey === user?.pubkey)
|
||||
.map((s) => ({ id: `s:${s.id}`, kind: SKATE_SPOT_KIND, pubkey: s.pubkey, identifier: s.identifier, title: s.title }));
|
||||
const shops = (allShops ?? []).filter((s) => s.pubkey === user?.pubkey)
|
||||
.map((s) => ({ id: `h:${s.id}`, kind: SKATE_SHOP_KIND, pubkey: s.pubkey, identifier: s.identifier, title: s.name }));
|
||||
return [...spots, ...shops];
|
||||
}, [allSpots, allShops, user?.pubkey]);
|
||||
const venueSpot = myVenues.find((s) => s.id === venueId);
|
||||
|
||||
// Hydrate the editor from the saved Rider Card the first time it loads.
|
||||
|
|
@ -566,7 +572,7 @@ export default function RiderCardPage() {
|
|||
['d', d],
|
||||
['p', recipientPubkey], ['t', 'skatepark-access'], ['alt', 'Skatepark Access Card'],
|
||||
['issuer', issuerName.trim() || venueSpot.title], ['access-level', accessLevel], ['theme', theme],
|
||||
['a', `${SKATE_SPOT_KIND}:${venueSpot.pubkey}:${venueSpot.identifier}`],
|
||||
['a', `${venueSpot.kind}:${venueSpot.pubkey}:${venueSpot.identifier}`],
|
||||
];
|
||||
if (validThru) tags.push(['valid-thru', validThru]);
|
||||
publishEvent({ kind: PARK_ACCESS_CARD_KIND, content: '', tags }, {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { useAuthor } from '@/hooks/useAuthor';
|
|||
import { useTranslation } from '@/contexts/language';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { RIDER_CARD_KIND, PARK_ACCESS_CARD_KIND, passExpiry } from '@/lib/riderCard';
|
||||
import { SKATE_SPOT_KIND, SKATE_CREW_KIND, CALENDAR_EVENT_KIND, CALENDAR_RSVP_KIND } from '@/lib/types';
|
||||
import { SKATE_SPOT_KIND, SKATE_SHOP_KIND, SKATE_CREW_KIND, CALENDAR_EVENT_KIND, CALENDAR_RSVP_KIND } from '@/lib/types';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
interface ParsedNaddr {
|
||||
|
|
@ -85,7 +85,7 @@ export default function VerifyCardPage() {
|
|||
venue = { status: 'mismatch', coord: aTag };
|
||||
const [k, pk, dTag] = aTag.split(':');
|
||||
const kindNum = Number(k);
|
||||
if ((kindNum === SKATE_SPOT_KIND || kindNum === CALENDAR_EVENT_KIND) && pk && dTag) {
|
||||
if ((kindNum === SKATE_SPOT_KIND || kindNum === SKATE_SHOP_KIND || kindNum === CALENDAR_EVENT_KIND) && pk && dTag) {
|
||||
try {
|
||||
const targets = await nostr.query(
|
||||
[{ kinds: [kindNum], authors: [pk], '#d': [dTag], limit: 1 }],
|
||||
|
|
|
|||
Loading…
Reference in a new issue