@passportsign/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/badge.d.ts +5 -0
  2. package/dist/badge.d.ts.map +1 -1
  3. package/dist/badge.js +8 -2
  4. package/dist/badge.js.map +1 -1
  5. package/dist/bind.d.ts.map +1 -1
  6. package/dist/bind.js +2 -8
  7. package/dist/bind.js.map +1 -1
  8. package/dist/bundle-fs.d.ts +16 -0
  9. package/dist/bundle-fs.d.ts.map +1 -0
  10. package/dist/bundle-fs.js +31 -0
  11. package/dist/bundle-fs.js.map +1 -0
  12. package/dist/bundle.d.ts +13 -5
  13. package/dist/bundle.d.ts.map +1 -1
  14. package/dist/bundle.js +18 -20
  15. package/dist/bundle.js.map +1 -1
  16. package/dist/canonical.d.ts.map +1 -1
  17. package/dist/canonical.js +3 -4
  18. package/dist/canonical.js.map +1 -1
  19. package/dist/classify.d.ts +68 -0
  20. package/dist/classify.d.ts.map +1 -0
  21. package/dist/classify.js +117 -0
  22. package/dist/classify.js.map +1 -0
  23. package/dist/dsse-common.d.ts +32 -0
  24. package/dist/dsse-common.d.ts.map +1 -0
  25. package/dist/dsse-common.js +26 -0
  26. package/dist/dsse-common.js.map +1 -0
  27. package/dist/dsse-web.d.ts +28 -0
  28. package/dist/dsse-web.d.ts.map +1 -0
  29. package/dist/dsse-web.js +81 -0
  30. package/dist/dsse-web.js.map +1 -0
  31. package/dist/dsse.d.ts +2 -26
  32. package/dist/dsse.d.ts.map +1 -1
  33. package/dist/dsse.js +2 -19
  34. package/dist/dsse.js.map +1 -1
  35. package/dist/encoding.d.ts +20 -0
  36. package/dist/encoding.d.ts.map +1 -0
  37. package/dist/encoding.js +88 -0
  38. package/dist/encoding.js.map +1 -0
  39. package/dist/github.js +2 -2
  40. package/dist/github.js.map +1 -1
  41. package/dist/index.d.ts +9 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +8 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/log/rekor.d.ts +1 -1
  46. package/dist/log/rekor.d.ts.map +1 -1
  47. package/dist/log/rekor.js +7 -10
  48. package/dist/log/rekor.js.map +1 -1
  49. package/dist/lookup.d.ts +46 -0
  50. package/dist/lookup.d.ts.map +1 -0
  51. package/dist/lookup.js +101 -0
  52. package/dist/lookup.js.map +1 -0
  53. package/dist/merkle.js +3 -3
  54. package/dist/merkle.js.map +1 -1
  55. package/dist/nonce.js +1 -1
  56. package/dist/nonce.js.map +1 -1
  57. package/dist/profile-index.d.ts +64 -0
  58. package/dist/profile-index.d.ts.map +1 -0
  59. package/dist/profile-index.js +161 -0
  60. package/dist/profile-index.js.map +1 -0
  61. package/dist/revoke.d.ts +30 -0
  62. package/dist/revoke.d.ts.map +1 -0
  63. package/dist/revoke.js +42 -0
  64. package/dist/revoke.js.map +1 -0
  65. package/dist/sdk-payload.d.ts.map +1 -1
  66. package/dist/sdk-payload.js +4 -6
  67. package/dist/sdk-payload.js.map +1 -1
  68. package/dist/statement.d.ts +41 -0
  69. package/dist/statement.d.ts.map +1 -1
  70. package/dist/statement.js +43 -0
  71. package/dist/statement.js.map +1 -1
  72. package/dist/submit.d.ts +3 -3
  73. package/dist/submit.d.ts.map +1 -1
  74. package/dist/submit.js +3 -14
  75. package/dist/submit.js.map +1 -1
  76. package/dist/verifier.d.ts.map +1 -1
  77. package/dist/verifier.js +4 -14
  78. package/dist/verifier.js.map +1 -1
  79. package/dist/web.d.ts +35 -0
  80. package/dist/web.d.ts.map +1 -0
  81. package/dist/web.js +35 -0
  82. package/dist/web.js.map +1 -0
  83. package/package.json +6 -2
  84. package/src/badge.ts +124 -113
  85. package/src/bind.ts +128 -137
  86. package/src/bundle-fs.ts +40 -0
  87. package/src/bundle.ts +138 -127
  88. package/src/canonical.ts +33 -33
  89. package/src/classify.ts +165 -0
  90. package/src/dsse-common.ts +45 -0
  91. package/src/dsse-web.ts +97 -0
  92. package/src/dsse.ts +63 -91
  93. package/src/encoding.ts +96 -0
  94. package/src/github.ts +196 -196
  95. package/src/index.ts +59 -2
  96. package/src/log/rekor.ts +330 -334
  97. package/src/lookup.ts +175 -0
  98. package/src/merkle.ts +187 -187
  99. package/src/nonce.ts +53 -53
  100. package/src/profile-index.ts +222 -0
  101. package/src/revoke.ts +67 -0
  102. package/src/sdk-payload.ts +60 -62
  103. package/src/statement.ts +203 -119
  104. package/src/submit.ts +38 -54
  105. package/src/verifier.ts +304 -317
  106. package/src/web.ts +175 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * The `passportsign-index.json` convention (roadmap v0.5.5).
3
+ *
4
+ * Public Rekor cannot be searched by predicateType, so discovery of a
5
+ * user's bindings flows through a JSON file the user publishes at the
6
+ * root of their profile repo (`github.com/<user>/<user>`, branch
7
+ * `main`). The file lists Rekor entry UUIDs for bindings *and*
8
+ * revocations — revocations are discoverable only through this file,
9
+ * which is why the schema carries them from version 1.
10
+ *
11
+ * The file is user-controlled: consumers (the `list` command, the
12
+ * badge service) must sanity-check every referenced entry against the
13
+ * log rather than trusting the file's contents.
14
+ */
15
+
16
+ import { PassportsignError } from './errors.js';
17
+
18
+ export const PROFILE_INDEX_VERSION = 1 as const;
19
+ export const PROFILE_INDEX_FILENAME = 'passportsign-index.json' as const;
20
+
21
+ /** Rekor entry UUIDs are 80 hex chars (16-byte tree-ID prefix + 32-byte entry hash). */
22
+ const REKOR_UUID = /^[0-9a-f]{80}$/;
23
+
24
+ export interface ProfileIndexBinding {
25
+ rekor_entry_hash: string;
26
+ /** ISO 8601; display convenience only — Rekor's integratedTime is authoritative. */
27
+ bound_at: string;
28
+ }
29
+
30
+ export interface ProfileIndexRevocation {
31
+ rekor_entry_hash: string;
32
+ /** UUID of the binding entry being revoked; absent = revokes all bindings for this user. */
33
+ revokes_rekor_entry_hash?: string;
34
+ /** ISO 8601; display convenience only. */
35
+ revoked_at: string;
36
+ }
37
+
38
+ export interface ProfileIndex {
39
+ version: typeof PROFILE_INDEX_VERSION;
40
+ github_username: string;
41
+ bindings: ProfileIndexBinding[];
42
+ revocations: ProfileIndexRevocation[];
43
+ }
44
+
45
+ export class ProfileIndexValidationError extends Error {
46
+ constructor(message: string) {
47
+ super(message);
48
+ this.name = 'ProfileIndexValidationError';
49
+ }
50
+ }
51
+
52
+ export function createProfileIndex(githubUsername: string): ProfileIndex {
53
+ if (githubUsername.length === 0) {
54
+ throw new TypeError('github_username: must be non-empty');
55
+ }
56
+ return {
57
+ version: PROFILE_INDEX_VERSION,
58
+ github_username: githubUsername,
59
+ bindings: [],
60
+ revocations: [],
61
+ };
62
+ }
63
+
64
+ function fail(message: string): never {
65
+ throw new ProfileIndexValidationError(message);
66
+ }
67
+
68
+ function assertRekorUuid(value: unknown, field: string): string {
69
+ if (typeof value !== 'string' || !REKOR_UUID.test(value)) {
70
+ fail(`${field}: expected 80-char lowercase hex Rekor entry UUID, got ${JSON.stringify(value)}`);
71
+ }
72
+ return value;
73
+ }
74
+
75
+ function assertIsoDate(value: unknown, field: string): string {
76
+ if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) {
77
+ fail(`${field}: expected ISO 8601 timestamp, got ${JSON.stringify(value)}`);
78
+ }
79
+ return value;
80
+ }
81
+
82
+ export function validateProfileIndex(raw: unknown): ProfileIndex {
83
+ if (typeof raw !== 'object' || raw === null) {
84
+ fail('index must be a JSON object');
85
+ }
86
+ const obj = raw as Record<string, unknown>;
87
+ if (obj['version'] !== PROFILE_INDEX_VERSION) {
88
+ fail(`version: expected ${PROFILE_INDEX_VERSION}, got ${JSON.stringify(obj['version'])}`);
89
+ }
90
+ const username = obj['github_username'];
91
+ if (typeof username !== 'string' || username.length === 0) {
92
+ fail('github_username: must be a non-empty string');
93
+ }
94
+ const bindingsRaw = obj['bindings'];
95
+ if (!Array.isArray(bindingsRaw)) {
96
+ fail('bindings: must be an array');
97
+ }
98
+ const revocationsRaw = obj['revocations'];
99
+ if (!Array.isArray(revocationsRaw)) {
100
+ fail('revocations: must be an array');
101
+ }
102
+
103
+ const bindings: ProfileIndexBinding[] = bindingsRaw.map((b, i) => {
104
+ if (typeof b !== 'object' || b === null) fail(`bindings[${i}]: must be an object`);
105
+ const rec = b as Record<string, unknown>;
106
+ return {
107
+ rekor_entry_hash: assertRekorUuid(rec['rekor_entry_hash'], `bindings[${i}].rekor_entry_hash`),
108
+ bound_at: assertIsoDate(rec['bound_at'], `bindings[${i}].bound_at`),
109
+ };
110
+ });
111
+
112
+ const revocations: ProfileIndexRevocation[] = revocationsRaw.map((r, i) => {
113
+ if (typeof r !== 'object' || r === null) fail(`revocations[${i}]: must be an object`);
114
+ const rec = r as Record<string, unknown>;
115
+ const out: ProfileIndexRevocation = {
116
+ rekor_entry_hash: assertRekorUuid(
117
+ rec['rekor_entry_hash'],
118
+ `revocations[${i}].rekor_entry_hash`,
119
+ ),
120
+ revoked_at: assertIsoDate(rec['revoked_at'], `revocations[${i}].revoked_at`),
121
+ };
122
+ if (rec['revokes_rekor_entry_hash'] !== undefined) {
123
+ out.revokes_rekor_entry_hash = assertRekorUuid(
124
+ rec['revokes_rekor_entry_hash'],
125
+ `revocations[${i}].revokes_rekor_entry_hash`,
126
+ );
127
+ }
128
+ return out;
129
+ });
130
+
131
+ return { version: PROFILE_INDEX_VERSION, github_username: username, bindings, revocations };
132
+ }
133
+
134
+ /** Append a binding; no-op (keeping the existing record) if the UUID is already listed. */
135
+ export function addBinding(index: ProfileIndex, binding: ProfileIndexBinding): ProfileIndex {
136
+ if (index.bindings.some((b) => b.rekor_entry_hash === binding.rekor_entry_hash)) {
137
+ return index;
138
+ }
139
+ return { ...index, bindings: [...index.bindings, binding] };
140
+ }
141
+
142
+ /** Append a revocation; no-op if the UUID is already listed. */
143
+ export function addRevocation(
144
+ index: ProfileIndex,
145
+ revocation: ProfileIndexRevocation,
146
+ ): ProfileIndex {
147
+ if (index.revocations.some((r) => r.rekor_entry_hash === revocation.rekor_entry_hash)) {
148
+ return index;
149
+ }
150
+ return { ...index, revocations: [...index.revocations, revocation] };
151
+ }
152
+
153
+ /**
154
+ * Union two indexes for the same user (e.g. the user's own file plus
155
+ * the operator overlay). Deduped by entry UUID; first occurrence wins.
156
+ */
157
+ export function mergeProfileIndexes(a: ProfileIndex, b: ProfileIndex): ProfileIndex {
158
+ if (a.github_username.toLowerCase() !== b.github_username.toLowerCase()) {
159
+ fail(
160
+ `cannot merge indexes for different users: ${a.github_username} vs ${b.github_username}`,
161
+ );
162
+ }
163
+ let merged = a;
164
+ for (const binding of b.bindings) merged = addBinding(merged, binding);
165
+ for (const revocation of b.revocations) merged = addRevocation(merged, revocation);
166
+ return merged;
167
+ }
168
+
169
+ export function profileIndexUrl(githubUsername: string): string {
170
+ return `https://raw.githubusercontent.com/${githubUsername}/${githubUsername}/main/${PROFILE_INDEX_FILENAME}`;
171
+ }
172
+
173
+ export interface FetchProfileIndexOptions {
174
+ fetch?: typeof fetch;
175
+ /** Override the URL (e.g. the operator overlay); defaults to the profile-repo convention. */
176
+ url?: string;
177
+ }
178
+
179
+ /**
180
+ * Fetch and validate a user's published index.
181
+ *
182
+ * Returns `null` on 404 (the user hasn't published one — an expected
183
+ * state, not an error). Malformed content throws
184
+ * `ProfileIndexValidationError`; transport/HTTP failures throw
185
+ * `PassportsignError('internal_error')`.
186
+ */
187
+ export async function fetchProfileIndex(
188
+ githubUsername: string,
189
+ opts: FetchProfileIndexOptions = {},
190
+ ): Promise<ProfileIndex | null> {
191
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
192
+ const url = opts.url ?? profileIndexUrl(githubUsername);
193
+
194
+ let response: Response;
195
+ try {
196
+ response = await fetchImpl(url);
197
+ } catch (err) {
198
+ throw new PassportsignError(
199
+ 'internal_error',
200
+ `profile-index fetch failed: ${err instanceof Error ? err.message : String(err)}`,
201
+ err,
202
+ );
203
+ }
204
+ if (response.status === 404) {
205
+ return null;
206
+ }
207
+ if (!response.ok) {
208
+ throw new PassportsignError(
209
+ 'internal_error',
210
+ `profile-index fetch returned ${response.status} for ${url}`,
211
+ );
212
+ }
213
+ let body: unknown;
214
+ try {
215
+ body = await response.json();
216
+ } catch (err) {
217
+ throw new ProfileIndexValidationError(
218
+ `profile-index at ${url} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
219
+ );
220
+ }
221
+ return validateProfileIndex(body);
222
+ }
package/src/revoke.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Revocation orchestrator (roadmap v0.5.2, spec §7).
3
+ *
4
+ * Mirror of `bind.ts`'s `prepareBinding` minus the GitHub gist check:
5
+ * revocation deliberately requires only a fresh proof from the same
6
+ * passport, so a user who lost their GitHub account can still revoke.
7
+ * The result feeds the same `submitBinding` Rekor path — a revocation
8
+ * is just another in-toto entry, with the `#revocation` predicateType.
9
+ */
10
+
11
+ import { canonicalize, canonicalSha256Hex } from './canonical.js';
12
+ import { base64ToBytes, sha256Hex } from './encoding.js';
13
+ import { PassportsignError } from './errors.js';
14
+ import {
15
+ buildRevocationStatement,
16
+ type PassportsignRevocationStatement,
17
+ } from './statement.js';
18
+
19
+ export interface PrepareRevocationInput {
20
+ github_username: string;
21
+ /** Base64-encoded zkPassport proof blob (fresh scan, same passport). */
22
+ proof_blob_b64: string;
23
+ /** From the SDK's `onResult` — must match the binding being revoked. */
24
+ unique_identifier: string;
25
+ /** Rekor entry UUID of the binding to revoke. */
26
+ revokes_rekor_entry_hash: string;
27
+ scope: string;
28
+ zkpassport_sdk_version: string;
29
+ }
30
+
31
+ export interface PreparedRevocation {
32
+ statement: PassportsignRevocationStatement;
33
+ statement_canonical: Uint8Array;
34
+ statement_sha256_hex: string;
35
+ proof_blob_b64: string;
36
+ proof_blob_sha256_hex: string;
37
+ }
38
+
39
+ export function prepareRevocation(input: PrepareRevocationInput): PreparedRevocation {
40
+ let proofBytes: Uint8Array;
41
+ try {
42
+ proofBytes = base64ToBytes(input.proof_blob_b64);
43
+ } catch (err) {
44
+ throw new PassportsignError('proof_invalid', 'proof_blob_b64 is not valid base64', err);
45
+ }
46
+ if (proofBytes.length === 0) {
47
+ throw new PassportsignError('proof_invalid', 'proof_blob_b64 decoded to zero bytes');
48
+ }
49
+ const proof_blob_sha256_hex = sha256Hex(proofBytes);
50
+
51
+ const statement = buildRevocationStatement({
52
+ github_username: input.github_username,
53
+ unique_identifier: input.unique_identifier,
54
+ revokes_rekor_entry_hash: input.revokes_rekor_entry_hash,
55
+ proof_blob_sha256: proof_blob_sha256_hex,
56
+ scope: input.scope,
57
+ zkpassport_sdk_version: input.zkpassport_sdk_version,
58
+ });
59
+
60
+ return {
61
+ statement,
62
+ statement_canonical: canonicalize(statement),
63
+ statement_sha256_hex: canonicalSha256Hex(statement),
64
+ proof_blob_b64: input.proof_blob_b64,
65
+ proof_blob_sha256_hex,
66
+ };
67
+ }
@@ -1,62 +1,60 @@
1
- /**
2
- * Pack/unpack the zkPassport SDK inputs that a verifier needs to re-run
3
- * proof verification offline.
4
- *
5
- * Rather than expanding the bundle schema, we re-purpose the existing
6
- * `proof_blob` field: it's the base64 of canonical JCS bytes of an
7
- * {@link SdkPayload} object. The statement's `proof_blob_sha256` already
8
- * binds those bytes to the rest of the binding — Day 5's hash check
9
- * carries through.
10
- */
11
-
12
- import { createHash } from 'node:crypto';
13
- import { canonicalize } from './canonical.js';
14
-
15
- export interface SdkPayload {
16
- /** The zkPassport SDK version that produced these proofs. */
17
- sdk_version: string;
18
- /** Array of ProofResult objects from onProofGenerated callbacks. */
19
- proofs: unknown[];
20
- /** The Query object from queryBuilder, in serialised form. */
21
- original_query: unknown;
22
- /** The QueryResult from the SDK's onResult callback. */
23
- query_result: unknown;
24
- /** Whether the proofs are mock (zkPassport dev mode). */
25
- dev_mode: boolean;
26
- }
27
-
28
- export interface PackedSdkPayload {
29
- /** Canonical bytes (RFC 8785 JCS UTF-8). */
30
- bytes: Uint8Array;
31
- /** Base64 of `bytes` — the value that goes into `bundle.proof_blob`. */
32
- b64: string;
33
- /** Lowercase-hex SHA-256 of `bytes` — the value that goes into the statement's `proof_blob_sha256`. */
34
- sha256Hex: string;
35
- }
36
-
37
- export function packSdkPayload(payload: SdkPayload): PackedSdkPayload {
38
- const bytes = canonicalize(payload);
39
- const b64 = Buffer.from(bytes).toString('base64');
40
- const sha256Hex = createHash('sha256').update(bytes).digest('hex');
41
- return { bytes, b64, sha256Hex };
42
- }
43
-
44
- export function unpackSdkPayload(b64: string): SdkPayload {
45
- const bytes = Buffer.from(b64, 'base64');
46
- const parsed = JSON.parse(bytes.toString('utf8')) as Record<string, unknown>;
47
- // Defensive shape check (cheap; the canonicalize round-trip would already catch shape issues elsewhere).
48
- if (
49
- typeof parsed['sdk_version'] !== 'string' ||
50
- typeof parsed['dev_mode'] !== 'boolean' ||
51
- !Array.isArray(parsed['proofs'])
52
- ) {
53
- throw new TypeError('unpackSdkPayload: not a valid SdkPayload shape');
54
- }
55
- return {
56
- sdk_version: parsed['sdk_version'],
57
- proofs: parsed['proofs'],
58
- original_query: parsed['original_query'],
59
- query_result: parsed['query_result'],
60
- dev_mode: parsed['dev_mode'],
61
- };
62
- }
1
+ /**
2
+ * Pack/unpack the zkPassport SDK inputs that a verifier needs to re-run
3
+ * proof verification offline.
4
+ *
5
+ * Rather than expanding the bundle schema, we re-purpose the existing
6
+ * `proof_blob` field: it's the base64 of canonical JCS bytes of an
7
+ * {@link SdkPayload} object. The statement's `proof_blob_sha256` already
8
+ * binds those bytes to the rest of the binding — Day 5's hash check
9
+ * carries through.
10
+ */
11
+
12
+ import { canonicalize } from './canonical.js';
13
+ import { base64ToBytes, bytesToBase64, bytesToUtf8, sha256Hex } from './encoding.js';
14
+
15
+ export interface SdkPayload {
16
+ /** The zkPassport SDK version that produced these proofs. */
17
+ sdk_version: string;
18
+ /** Array of ProofResult objects from onProofGenerated callbacks. */
19
+ proofs: unknown[];
20
+ /** The Query object from queryBuilder, in serialised form. */
21
+ original_query: unknown;
22
+ /** The QueryResult from the SDK's onResult callback. */
23
+ query_result: unknown;
24
+ /** Whether the proofs are mock (zkPassport dev mode). */
25
+ dev_mode: boolean;
26
+ }
27
+
28
+ export interface PackedSdkPayload {
29
+ /** Canonical bytes (RFC 8785 JCS UTF-8). */
30
+ bytes: Uint8Array;
31
+ /** Base64 of `bytes` — the value that goes into `bundle.proof_blob`. */
32
+ b64: string;
33
+ /** Lowercase-hex SHA-256 of `bytes` — the value that goes into the statement's `proof_blob_sha256`. */
34
+ sha256Hex: string;
35
+ }
36
+
37
+ export function packSdkPayload(payload: SdkPayload): PackedSdkPayload {
38
+ const bytes = canonicalize(payload);
39
+ return { bytes, b64: bytesToBase64(bytes), sha256Hex: sha256Hex(bytes) };
40
+ }
41
+
42
+ export function unpackSdkPayload(b64: string): SdkPayload {
43
+ const bytes = base64ToBytes(b64);
44
+ const parsed = JSON.parse(bytesToUtf8(bytes)) as Record<string, unknown>;
45
+ // Defensive shape check (cheap; the canonicalize round-trip would already catch shape issues elsewhere).
46
+ if (
47
+ typeof parsed['sdk_version'] !== 'string' ||
48
+ typeof parsed['dev_mode'] !== 'boolean' ||
49
+ !Array.isArray(parsed['proofs'])
50
+ ) {
51
+ throw new TypeError('unpackSdkPayload: not a valid SdkPayload shape');
52
+ }
53
+ return {
54
+ sdk_version: parsed['sdk_version'],
55
+ proofs: parsed['proofs'],
56
+ original_query: parsed['original_query'],
57
+ query_result: parsed['query_result'],
58
+ dev_mode: parsed['dev_mode'],
59
+ };
60
+ }