Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The owner is a Principal Software Engineer in Bangladesh building this as a port
## Rules

- **Talk only to `/v1`.** Same address as the API in production (Caddy routes `/v1` to it) and in development (Vite proxy). No CORS, no other backends.
- **Types come from the API's OpenAPI spec** (`npm run api:types` -> `src/api/generated`, never edit by hand). When the API changes, refresh `openapi.json` and regenerate. pairkit's `/v1/pair/*` routes have no response models yet; their types are hand-written in `src/api/types.ts`.
- **Types come from the API's OpenAPI spec** (`npm run api:types` -> `src/api/generated`, never edit by hand). When the API changes, refresh `openapi.json` and regenerate. That includes pairkit's `/v1/pair/*` routes; `PairGrant` adds the `space_id` ScribbleScan's grant hook puts in the grant.
- **No UI framework**, by choice: plain DOM via `h()`/`render()` in `src/dom.ts`. They flatten nested arrays and drop null/false, so views can write `cond ? h(...) : null`.
- **Only use things with a future** (the owner's rule): nothing deprecated, unmaintained or near end of life. Node 24 LTS, TypeScript 7, Vite 8, Vitest, Biome. Check maintenance before adding a dependency, and prefer none.
- Dates are DD/MM/YYYY text fields converted to ISO (native date inputs show US order in many browsers). Money uses lakh grouping (`en-IN`). The current month comes from the local clock, not UTC.
Expand Down
256 changes: 244 additions & 12 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/OfferCreated"
}
}
}
},
Expand Down Expand Up @@ -67,7 +69,9 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/Claimed"
}
}
}
},
Expand Down Expand Up @@ -116,7 +120,9 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/OfferStatus"
}
}
}
},
Expand Down Expand Up @@ -162,11 +168,9 @@
],
"responses": {
"200": {
"description": "Successful Response",
"description": "Server-sent events: state, claimed, approved, denied, collected, expired",
"content": {
"application/json": {
"schema": {}
}
"text/event-stream": {}
}
},
"422": {
Expand Down Expand Up @@ -205,7 +209,9 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/OfferStatus"
}
}
}
},
Expand Down Expand Up @@ -245,7 +251,9 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/OfferStatus"
}
}
}
},
Expand Down Expand Up @@ -294,7 +302,9 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/Collected"
}
}
}
},
Expand Down Expand Up @@ -323,7 +333,13 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"schema": {
"items": {
"$ref": "#/components/schemas/LinkedDeviceOut"
},
"type": "array",
"title": "Response Devices V1 Pair Devices Get"
}
}
}
}
Expand Down Expand Up @@ -353,7 +369,9 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/LinkedDeviceOut"
}
}
}
},
Expand Down Expand Up @@ -1357,6 +1375,48 @@
],
"title": "ClaimIn"
},
"Claimed": {
"properties": {
"offer_id": {
"type": "string",
"title": "Offer Id"
},
"claimer_key": {
"type": "string",
"title": "Claimer Key",
"description": "the scanning device's private key: X-Pairkit-Key / ?key="
},
"grant_to": {
"$ref": "#/components/schemas/GrantTo"
},
"scopes": {
"items": {
"type": "string"
},
"type": "array",
"title": "Scopes"
},
"shower": {
"type": "string",
"title": "Shower",
"description": "how the device showing the QR describes itself"
},
"expires_at": {
"type": "string",
"title": "Expires At"
}
},
"type": "object",
"required": [
"offer_id",
"claimer_key",
"grant_to",
"scopes",
"shower",
"expires_at"
],
"title": "Claimed"
},
"ClientConfig": {
"properties": {
"auth": {
Expand Down Expand Up @@ -1408,6 +1468,33 @@
"title": "ClientConfig",
"description": "Which sign-in this server uses. For \"oidc\", where and how to sign in."
},
"Collected": {
"properties": {
"token": {
"type": "string",
"title": "Token"
},
"expires_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Expires At"
}
},
"additionalProperties": true,
"type": "object",
"required": [
"token",
"expires_at"
],
"title": "Collected",
"description": "The grant from the app's issue_grant hook, plus whatever it put in Grant.data."
},
"CreateOfferIn": {
"properties": {
"grant_to": {
Expand Down Expand Up @@ -1770,6 +1857,46 @@
"title": "Issue",
"description": "A business rule the document doesn't pass yet, e.g. rule=\"missing\", detail=\"total\"."
},
"LinkedDeviceOut": {
"properties": {
"id": {
"type": "string",
"title": "Id"
},
"device": {
"type": "string",
"title": "Device"
},
"scopes": {
"items": {
"type": "string"
},
"type": "array",
"title": "Scopes"
},
"linked_at": {
"type": "string",
"title": "Linked At"
},
"via": {
"$ref": "#/components/schemas/GrantTo"
},
"active": {
"type": "boolean",
"title": "Active"
}
},
"type": "object",
"required": [
"id",
"device",
"scopes",
"linked_at",
"via",
"active"
],
"title": "LinkedDeviceOut"
},
"Me": {
"properties": {
"id": {
Expand Down Expand Up @@ -1811,6 +1938,99 @@
],
"title": "MonthTotal"
},
"OfferCreated": {
"properties": {
"offer_id": {
"type": "string",
"title": "Offer Id"
},
"secret": {
"type": "string",
"title": "Secret",
"description": "goes in the QR code, in the URL fragment"
},
"shower_key": {
"type": "string",
"title": "Shower Key",
"description": "this device's private key: X-Pairkit-Key / ?key="
},
"expires_at": {
"type": "string",
"title": "Expires At"
},
"qr_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Qr Url",
"description": "ready-to-encode scan link, when the router has a scan_url"
}
},
"type": "object",
"required": [
"offer_id",
"secret",
"shower_key",
"expires_at"
],
"title": "OfferCreated"
},
"OfferStatus": {
"properties": {
"offer_id": {
"type": "string",
"title": "Offer Id"
},
"state": {
"$ref": "#/components/schemas/State"
},
"grant_to": {
"$ref": "#/components/schemas/GrantTo"
},
"scopes": {
"items": {
"type": "string"
},
"type": "array",
"title": "Scopes"
},
"expires_at": {
"type": "string",
"title": "Expires At"
},
"shower": {
"type": "string",
"title": "Shower"
},
"scanner": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Scanner"
}
},
"type": "object",
"required": [
"offer_id",
"state",
"grant_to",
"scopes",
"expires_at",
"shower",
"scanner"
],
"title": "OfferStatus"
},
"PairOffer": {
"properties": {
"offer_id": {
Expand Down Expand Up @@ -1921,6 +2141,18 @@
],
"title": "SpaceOut"
},
"State": {
"type": "string",
"enum": [
"pending",
"claimed",
"approved",
"collected",
"denied",
"expired"
],
"title": "State"
},
"Summary": {
"properties": {
"month": {
Expand Down
Loading