Skip to content

Commit a6d9f0a

Browse files
authored
Merge pull request #178 from PortalTechnologiesInc/feat/age-verification-rest
feat(portal-rest): add age verification
2 parents 2924714 + ec73e7c commit a6d9f0a

12 files changed

Lines changed: 455 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1414
- `GET /info` now includes `version` and `git_commit` fields (previously only in `GET /version`). `GET /version` kept for backward compatibility.
1515

1616
#### Added
17+
- **Age verification**: `POST /verification/sessions` creates a browser-based age verification session AND automatically starts listening for the verification token in a single call. Returns session info plus a `stream_id` to poll for the Cashu token result. Requires `[verification] api_key` in config. Relay URLs default to the `[nostr]` config if not provided.
18+
- **Verification token request**: `POST /verification/token` requests a verification token from a user who already holds one (e.g. mobile app verified users). Returns a `stream_id` for async polling.
19+
- TS SDK: `createVerificationSession()` now returns `AsyncOperation<CashuResponseStatus>` — use `poll()` or `done` to wait for the token. `requestVerificationToken()` for the mobile flow.
20+
- Example: `examples/age-verification.js` — simplified single-call age verification flow
1721
- **NIP-05 auto-registration**: if `PORTAL__PROFILE__NIP05` is set to a `@getportal.cc` address, the daemon registers it with the Portal profile service at startup (one-time, cached in `~/.portal-rest/nip05.registered`). Self-hosted domains are set in the Nostr profile only, no external call.
1822

1923
---

crates/portal-rest/clients/ts/src/client.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
IssueJwtResponse,
2424
VerifyJwtResponse,
2525
CashuResponseStatus,
26+
VerificationSessionResponse,
2627
WalletInfoResponse,
2728
VersionResponse,
2829
InfoResponse,
@@ -750,4 +751,49 @@ export class PortalClient {
750751
const q = after !== undefined ? `?after=${after}` : '';
751752
return this.get<EventsResponse>(`/events/${encodeURIComponent(streamId)}${q}`);
752753
}
754+
755+
// ---- Verification ----
756+
757+
/**
758+
* Initiate a browser-based age verification session.
759+
*
760+
* Creates the verification session AND automatically starts listening for the
761+
* verification token. Returns session info plus an `AsyncOperation` — use
762+
* `poll()` or `done` to wait for the Cashu token result.
763+
*
764+
* Requires `[verification] api_key` in portal-rest config.
765+
*/
766+
public async createVerificationSession(relayUrls?: string[]): Promise<
767+
VerificationSessionResponse & AsyncOperation<CashuResponseStatus>
768+
> {
769+
const resp = await this.post<VerificationSessionResponse>('/verification/sessions', {
770+
relays: relayUrls,
771+
});
772+
const done = this.registerStream(resp.stream_id).then(
773+
(event) => event.status as CashuResponseStatus
774+
);
775+
return { ...resp, streamId: resp.stream_id, done };
776+
}
777+
778+
// ---- Verification Token ----
779+
780+
/**
781+
* Request a Cashu token from a recipient Portal wallet.
782+
*
783+
* Uses the Portal mint (`https://mint.getportal.cc`) with unit `multi`.
784+
* Returns the `stream_id`; poll via `getEvents(streamId)` or use `onEvent`.
785+
*/
786+
public async requestVerificationToken(
787+
recipientKey: string,
788+
subkeys: string[]
789+
): Promise<AsyncOperation<CashuResponseStatus>> {
790+
const resp = await this.post<{ stream_id: string }>('/verification/token', {
791+
recipient_key: recipientKey,
792+
subkeys,
793+
});
794+
const done = this.registerStream(resp.stream_id).then(
795+
(event) => event.status as CashuResponseStatus
796+
);
797+
return { streamId: resp.stream_id, done };
798+
}
753799
}

crates/portal-rest/clients/ts/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ export {
6060
BurnCashuRequest,
6161
CashuResponseStatus,
6262

63+
// Verification
64+
CreateVerificationSessionRequest,
65+
VerificationSessionResponse,
66+
67+
// Portal Token
68+
RequestVerificationTokenRequest,
69+
6370
// Relays
6471
RelayRequest,
6572

crates/portal-rest/clients/ts/src/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,28 @@ export type CashuResponseStatus =
321321
| { status: 'insufficient_funds' }
322322
| { status: 'rejected'; reason?: string };
323323

324+
// ---- Verification ----
325+
326+
export interface CreateVerificationSessionRequest {
327+
/** Relay URLs to use for the verification session. Defaults to the relays configured in the [nostr] section of the config if omitted. */
328+
relays?: string[];
329+
}
330+
331+
export interface VerificationSessionResponse {
332+
session_id: string;
333+
session_url: string;
334+
ephemeral_npub: string;
335+
expires_at: number;
336+
stream_id: string;
337+
}
338+
339+
// ---- Portal Token (Cashu from Portal wallet) ----
340+
341+
export interface RequestVerificationTokenRequest {
342+
recipient_key: string;
343+
subkeys: string[];
344+
}
345+
324346
// ---- Relays ----
325347

326348
export interface RelayRequest {

crates/portal-rest/example.config.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ ln_backend = "none"
5252
path = "portal-rest.db"
5353

5454

55+
## Age verification settings. Required for the POST /verification/sessions endpoint.
56+
## Get your API key from https://verify.getportal.cc
57+
# [verification]
58+
# api_key = "your-api-key-here"
59+
60+
5561
## Optional Nostr profile metadata. Set any combination of fields to publish
5662
## your profile on the Nostr network at startup. Omit the section or leave
5763
## fields commented out to skip.

crates/portal-rest/openapi.yaml

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,53 @@ components:
409409
type: string
410410
additionalProperties: true
411411

412+
CreateVerificationSessionRequest:
413+
type: object
414+
properties:
415+
relays:
416+
type: array
417+
items:
418+
type: string
419+
description: |
420+
Relay WebSocket URLs for the verification session.
421+
Defaults to the relays configured in the [nostr] section of the config if omitted.
422+
423+
VerificationSessionResponse:
424+
type: object
425+
required: [session_id, session_url, ephemeral_npub, expires_at, stream_id]
426+
properties:
427+
session_id:
428+
type: string
429+
description: Unique verification session identifier
430+
session_url:
431+
type: string
432+
description: URL to redirect the user to for verification
433+
ephemeral_npub:
434+
type: string
435+
description: Ephemeral Nostr public key for this session
436+
expires_at:
437+
type: integer
438+
format: uint64
439+
description: Unix timestamp when the session expires
440+
stream_id:
441+
type: string
442+
description: Stream ID to poll for the verification token result via GET /events/{stream_id}
443+
444+
RequestVerificationTokenRequest:
445+
type: object
446+
required: [recipient_key, subkeys]
447+
properties:
448+
recipient_key:
449+
type: string
450+
description: Hex-encoded recipient public key
451+
subkeys:
452+
type: array
453+
items:
454+
type: string
455+
description: Optional subkeys to include
456+
description: |
457+
Request a verification token from a recipient Portal wallet.
458+
412459
paths:
413460
/health:
414461
get:
@@ -928,3 +975,70 @@ paths:
928975
$ref: '#/components/schemas/EventsResponse'
929976
"404":
930977
description: Stream not found
978+
979+
980+
/verification/sessions:
981+
post:
982+
tags:
983+
- Verification
984+
summary: Initiate a browser-based age verification session
985+
description: |
986+
Creates a verification session AND automatically starts listening for the
987+
verification token. Returns session info (including `session_url` to redirect
988+
the user to) plus a `stream_id` to poll for the Cashu token result.
989+
Poll `GET /events/{stream_id}` for a `cashu_response` event containing the token.
990+
Requires `[verification] api_key` in the portal-rest configuration.
991+
requestBody:
992+
required: false
993+
content:
994+
application/json:
995+
schema:
996+
$ref: '#/components/schemas/CreateVerificationSessionRequest'
997+
responses:
998+
"200":
999+
description: Verification session created
1000+
content:
1001+
application/json:
1002+
schema:
1003+
allOf:
1004+
- $ref: '#/components/schemas/ApiResponse'
1005+
- properties:
1006+
data:
1007+
$ref: '#/components/schemas/VerificationSessionResponse'
1008+
"400":
1009+
description: Verification not configured or invalid request
1010+
"500":
1011+
description: Verification service error
1012+
1013+
/verification/token:
1014+
post:
1015+
tags:
1016+
- Verification
1017+
summary: Request a verification token from a user who already holds one
1018+
description: |
1019+
Requests a verification token (Cashu) from a recipient who already has a token
1020+
in their Portal wallet (e.g. a mobile app user who completed verification previously).
1021+
Uses the Portal mint (`https://mint.getportal.cc`) with unit `multi`. Returns a
1022+
stream_id for polling. Poll GET /events/{stream_id} for the `cashu_response` event.
1023+
1024+
For new browser-based verification, use `POST /verification/sessions` instead — it
1025+
handles the full flow (session creation + token listening) in a single call.
1026+
requestBody:
1027+
required: true
1028+
content:
1029+
application/json:
1030+
schema:
1031+
$ref: '#/components/schemas/RequestVerificationTokenRequest'
1032+
responses:
1033+
"201":
1034+
description: Verification token request initiated
1035+
content:
1036+
application/json:
1037+
schema:
1038+
allOf:
1039+
- $ref: '#/components/schemas/ApiResponse'
1040+
- properties:
1041+
data:
1042+
$ref: '#/components/schemas/StreamIdResponse'
1043+
"400":
1044+
description: Invalid recipient key or subkeys

crates/portal-rest/src/command.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ pub struct RequestCashuRequest {
7474
pub amount: u64,
7575
}
7676

77+
#[derive(Debug, Deserialize)]
78+
pub struct CreateVerificationSessionRequest {
79+
/// Relay URLs to use for the verification session. Defaults to
80+
/// the relays configured in the `[nostr]` section of the config if not set.
81+
pub relays: Option<Vec<String>>,
82+
}
83+
84+
#[derive(Debug, Deserialize)]
85+
pub struct RequestVerificationTokenRequest {
86+
pub recipient_key: String,
87+
pub subkeys: Vec<String>,
88+
}
89+
7790
#[derive(Debug, Deserialize)]
7891
pub struct SendCashuDirectRequest {
7992
pub main_key: String,

crates/portal-rest/src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ pub struct Settings {
1818
pub profile: ProfileSettings,
1919
#[serde(default)]
2020
pub logging: LoggingSettings,
21+
#[serde(default)]
22+
pub verification: Option<VerificationSettings>,
23+
}
24+
25+
#[derive(Deserialize, Debug, Clone)]
26+
pub struct VerificationSettings {
27+
pub api_key: String,
2128
}
2229

2330
#[derive(Deserialize, Debug, Clone)]

0 commit comments

Comments
 (0)