@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
package/src/github.ts CHANGED
@@ -1,196 +1,196 @@
1
- /**
2
- * GitHub gist control check per spec §3 step 5 and §14 "The gist control check."
3
- *
4
- * The honest semantic claim is: at the moment we checked, the named user
5
- * controlled a public gist with the expected filename and content. We
6
- * capture `html_url`, `updated_at`, and a SHA-256 of the content so the
7
- * evidence is independently re-checkable later (e.g. via the Wayback
8
- * Machine).
9
- *
10
- * The optional `token` is **purely for rate-limit headroom** — it
11
- * carries zero special access. Unauth'd: 60 req/hr; with token: 5000.
12
- */
13
-
14
- import { createHash } from 'node:crypto';
15
- import { PassportsignError } from './errors.js';
16
-
17
- export interface GistEvidence {
18
- url: string;
19
- content_sha256: string;
20
- updated_at: string;
21
- }
22
-
23
- export interface CheckGistOptions {
24
- username: string;
25
- expected_filename: string;
26
- expected_content: string;
27
- not_before: Date;
28
- token?: string;
29
- fetch?: typeof fetch;
30
- baseUrl?: string;
31
- }
32
-
33
- interface GistFile {
34
- filename?: string;
35
- content?: string;
36
- }
37
-
38
- interface GistSummary {
39
- id: string;
40
- html_url: string;
41
- updated_at: string;
42
- owner?: { login?: string } | null;
43
- files: Record<string, GistFile>;
44
- }
45
-
46
- const DEFAULT_BASE_URL = 'https://api.github.com';
47
- const GIST_LIST_PER_PAGE = 100;
48
-
49
- function sha256Hex(content: string): string {
50
- return createHash('sha256').update(content, 'utf8').digest('hex');
51
- }
52
-
53
- function authHeaders(token: string | undefined): Record<string, string> {
54
- return {
55
- Accept: 'application/vnd.github+json',
56
- 'X-GitHub-Api-Version': '2022-11-28',
57
- 'User-Agent': 'passportsign-cli',
58
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
59
- };
60
- }
61
-
62
- export async function checkGistControl(opts: CheckGistOptions): Promise<GistEvidence> {
63
- const fetchImpl = opts.fetch ?? globalThis.fetch;
64
- const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
65
- const headers = authHeaders(opts.token);
66
-
67
- if (opts.username.length === 0) {
68
- throw new PassportsignError('username_invalid', 'username must be non-empty');
69
- }
70
-
71
- // Step 1: list the user's gists, filter by filename.
72
- const listUrl = `${baseUrl}/users/${encodeURIComponent(opts.username)}/gists?per_page=${GIST_LIST_PER_PAGE}`;
73
- let listResponse: Response;
74
- try {
75
- listResponse = await fetchImpl(listUrl, { headers });
76
- } catch (err) {
77
- throw new PassportsignError(
78
- 'internal_error',
79
- `GitHub list-gists request failed: ${err instanceof Error ? err.message : String(err)}`,
80
- err,
81
- );
82
- }
83
-
84
- if (listResponse.status === 404) {
85
- throw new PassportsignError(
86
- 'username_invalid',
87
- `GitHub user '${opts.username}' not found`,
88
- );
89
- }
90
- if (!listResponse.ok) {
91
- throw new PassportsignError(
92
- 'internal_error',
93
- `GitHub list-gists returned HTTP ${listResponse.status}`,
94
- );
95
- }
96
-
97
- let listBody: unknown;
98
- try {
99
- listBody = await listResponse.json();
100
- } catch (err) {
101
- throw new PassportsignError(
102
- 'internal_error',
103
- 'GitHub list-gists returned non-JSON',
104
- err,
105
- );
106
- }
107
- if (!Array.isArray(listBody)) {
108
- throw new PassportsignError(
109
- 'internal_error',
110
- 'GitHub list-gists did not return an array',
111
- );
112
- }
113
-
114
- const matches = (listBody as GistSummary[])
115
- .filter((g) => g && typeof g === 'object' && opts.expected_filename in (g.files ?? {}))
116
- .sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1));
117
-
118
- const match = matches[0];
119
- if (!match) {
120
- throw new PassportsignError(
121
- 'gist_not_found',
122
- `no public gist owned by '${opts.username}' contains file '${opts.expected_filename}'`,
123
- );
124
- }
125
-
126
- // Step 2: re-fetch the gist by id to get the full content.
127
- let detailResponse: Response;
128
- try {
129
- detailResponse = await fetchImpl(`${baseUrl}/gists/${match.id}`, { headers });
130
- } catch (err) {
131
- throw new PassportsignError(
132
- 'internal_error',
133
- `GitHub get-gist request failed: ${err instanceof Error ? err.message : String(err)}`,
134
- err,
135
- );
136
- }
137
- if (!detailResponse.ok) {
138
- throw new PassportsignError(
139
- 'internal_error',
140
- `GitHub get-gist returned HTTP ${detailResponse.status}`,
141
- );
142
- }
143
-
144
- let detail: GistSummary;
145
- try {
146
- detail = (await detailResponse.json()) as GistSummary;
147
- } catch (err) {
148
- throw new PassportsignError('internal_error', 'GitHub get-gist returned non-JSON', err);
149
- }
150
-
151
- // Step 3: owner check (case-insensitive per spec §10 row 7).
152
- const ownerLogin = detail.owner?.login;
153
- if (!ownerLogin || ownerLogin.toLowerCase() !== opts.username.toLowerCase()) {
154
- throw new PassportsignError(
155
- 'gist_wrong_owner',
156
- `gist ${match.id} owner '${ownerLogin ?? 'unknown'}' does not match expected '${opts.username}'`,
157
- );
158
- }
159
-
160
- // Step 4: content exact match.
161
- const file = (detail.files ?? {})[opts.expected_filename];
162
- const content = file?.content;
163
- if (typeof content !== 'string') {
164
- throw new PassportsignError(
165
- 'gist_wrong_content',
166
- `gist ${match.id} has no readable content for '${opts.expected_filename}'`,
167
- );
168
- }
169
- if (content !== opts.expected_content) {
170
- throw new PassportsignError(
171
- 'gist_wrong_content',
172
- `gist ${match.id} content does not exactly match the expected nonce`,
173
- );
174
- }
175
-
176
- // Step 5: freshness — gist's updated_at must be at/after init.
177
- const updatedAtMs = Date.parse(detail.updated_at);
178
- if (Number.isNaN(updatedAtMs)) {
179
- throw new PassportsignError(
180
- 'internal_error',
181
- `gist ${match.id} updated_at is unparseable: ${detail.updated_at}`,
182
- );
183
- }
184
- if (updatedAtMs < opts.not_before.getTime()) {
185
- throw new PassportsignError(
186
- 'gist_predates_init',
187
- `gist ${match.id} updated_at (${detail.updated_at}) predates init (${opts.not_before.toISOString()})`,
188
- );
189
- }
190
-
191
- return {
192
- url: detail.html_url,
193
- content_sha256: sha256Hex(content),
194
- updated_at: detail.updated_at,
195
- };
196
- }
1
+ /**
2
+ * GitHub gist control check per spec §3 step 5 and §14 "The gist control check."
3
+ *
4
+ * The honest semantic claim is: at the moment we checked, the named user
5
+ * controlled a public gist with the expected filename and content. We
6
+ * capture `html_url`, `updated_at`, and a SHA-256 of the content so the
7
+ * evidence is independently re-checkable later (e.g. via the Wayback
8
+ * Machine).
9
+ *
10
+ * The optional `token` is **purely for rate-limit headroom** — it
11
+ * carries zero special access. Unauth'd: 60 req/hr; with token: 5000.
12
+ */
13
+
14
+ import { sha256Hex as sha256HexOfBytes, utf8ToBytes } from './encoding.js';
15
+ import { PassportsignError } from './errors.js';
16
+
17
+ export interface GistEvidence {
18
+ url: string;
19
+ content_sha256: string;
20
+ updated_at: string;
21
+ }
22
+
23
+ export interface CheckGistOptions {
24
+ username: string;
25
+ expected_filename: string;
26
+ expected_content: string;
27
+ not_before: Date;
28
+ token?: string;
29
+ fetch?: typeof fetch;
30
+ baseUrl?: string;
31
+ }
32
+
33
+ interface GistFile {
34
+ filename?: string;
35
+ content?: string;
36
+ }
37
+
38
+ interface GistSummary {
39
+ id: string;
40
+ html_url: string;
41
+ updated_at: string;
42
+ owner?: { login?: string } | null;
43
+ files: Record<string, GistFile>;
44
+ }
45
+
46
+ const DEFAULT_BASE_URL = 'https://api.github.com';
47
+ const GIST_LIST_PER_PAGE = 100;
48
+
49
+ function sha256Hex(content: string): string {
50
+ return sha256HexOfBytes(utf8ToBytes(content));
51
+ }
52
+
53
+ function authHeaders(token: string | undefined): Record<string, string> {
54
+ return {
55
+ Accept: 'application/vnd.github+json',
56
+ 'X-GitHub-Api-Version': '2022-11-28',
57
+ 'User-Agent': 'passportsign-cli',
58
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
59
+ };
60
+ }
61
+
62
+ export async function checkGistControl(opts: CheckGistOptions): Promise<GistEvidence> {
63
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
64
+ const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
65
+ const headers = authHeaders(opts.token);
66
+
67
+ if (opts.username.length === 0) {
68
+ throw new PassportsignError('username_invalid', 'username must be non-empty');
69
+ }
70
+
71
+ // Step 1: list the user's gists, filter by filename.
72
+ const listUrl = `${baseUrl}/users/${encodeURIComponent(opts.username)}/gists?per_page=${GIST_LIST_PER_PAGE}`;
73
+ let listResponse: Response;
74
+ try {
75
+ listResponse = await fetchImpl(listUrl, { headers });
76
+ } catch (err) {
77
+ throw new PassportsignError(
78
+ 'internal_error',
79
+ `GitHub list-gists request failed: ${err instanceof Error ? err.message : String(err)}`,
80
+ err,
81
+ );
82
+ }
83
+
84
+ if (listResponse.status === 404) {
85
+ throw new PassportsignError(
86
+ 'username_invalid',
87
+ `GitHub user '${opts.username}' not found`,
88
+ );
89
+ }
90
+ if (!listResponse.ok) {
91
+ throw new PassportsignError(
92
+ 'internal_error',
93
+ `GitHub list-gists returned HTTP ${listResponse.status}`,
94
+ );
95
+ }
96
+
97
+ let listBody: unknown;
98
+ try {
99
+ listBody = await listResponse.json();
100
+ } catch (err) {
101
+ throw new PassportsignError(
102
+ 'internal_error',
103
+ 'GitHub list-gists returned non-JSON',
104
+ err,
105
+ );
106
+ }
107
+ if (!Array.isArray(listBody)) {
108
+ throw new PassportsignError(
109
+ 'internal_error',
110
+ 'GitHub list-gists did not return an array',
111
+ );
112
+ }
113
+
114
+ const matches = (listBody as GistSummary[])
115
+ .filter((g) => g && typeof g === 'object' && opts.expected_filename in (g.files ?? {}))
116
+ .sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1));
117
+
118
+ const match = matches[0];
119
+ if (!match) {
120
+ throw new PassportsignError(
121
+ 'gist_not_found',
122
+ `no public gist owned by '${opts.username}' contains file '${opts.expected_filename}'`,
123
+ );
124
+ }
125
+
126
+ // Step 2: re-fetch the gist by id to get the full content.
127
+ let detailResponse: Response;
128
+ try {
129
+ detailResponse = await fetchImpl(`${baseUrl}/gists/${match.id}`, { headers });
130
+ } catch (err) {
131
+ throw new PassportsignError(
132
+ 'internal_error',
133
+ `GitHub get-gist request failed: ${err instanceof Error ? err.message : String(err)}`,
134
+ err,
135
+ );
136
+ }
137
+ if (!detailResponse.ok) {
138
+ throw new PassportsignError(
139
+ 'internal_error',
140
+ `GitHub get-gist returned HTTP ${detailResponse.status}`,
141
+ );
142
+ }
143
+
144
+ let detail: GistSummary;
145
+ try {
146
+ detail = (await detailResponse.json()) as GistSummary;
147
+ } catch (err) {
148
+ throw new PassportsignError('internal_error', 'GitHub get-gist returned non-JSON', err);
149
+ }
150
+
151
+ // Step 3: owner check (case-insensitive per spec §10 row 7).
152
+ const ownerLogin = detail.owner?.login;
153
+ if (!ownerLogin || ownerLogin.toLowerCase() !== opts.username.toLowerCase()) {
154
+ throw new PassportsignError(
155
+ 'gist_wrong_owner',
156
+ `gist ${match.id} owner '${ownerLogin ?? 'unknown'}' does not match expected '${opts.username}'`,
157
+ );
158
+ }
159
+
160
+ // Step 4: content exact match.
161
+ const file = (detail.files ?? {})[opts.expected_filename];
162
+ const content = file?.content;
163
+ if (typeof content !== 'string') {
164
+ throw new PassportsignError(
165
+ 'gist_wrong_content',
166
+ `gist ${match.id} has no readable content for '${opts.expected_filename}'`,
167
+ );
168
+ }
169
+ if (content !== opts.expected_content) {
170
+ throw new PassportsignError(
171
+ 'gist_wrong_content',
172
+ `gist ${match.id} content does not exactly match the expected nonce`,
173
+ );
174
+ }
175
+
176
+ // Step 5: freshness — gist's updated_at must be at/after init.
177
+ const updatedAtMs = Date.parse(detail.updated_at);
178
+ if (Number.isNaN(updatedAtMs)) {
179
+ throw new PassportsignError(
180
+ 'internal_error',
181
+ `gist ${match.id} updated_at is unparseable: ${detail.updated_at}`,
182
+ );
183
+ }
184
+ if (updatedAtMs < opts.not_before.getTime()) {
185
+ throw new PassportsignError(
186
+ 'gist_predates_init',
187
+ `gist ${match.id} updated_at (${detail.updated_at}) predates init (${opts.not_before.toISOString()})`,
188
+ );
189
+ }
190
+
191
+ return {
192
+ url: detail.html_url,
193
+ content_sha256: sha256Hex(content),
194
+ updated_at: detail.updated_at,
195
+ };
196
+ }
package/src/index.ts CHANGED
@@ -14,23 +14,29 @@ export {
14
14
  export {
15
15
  IN_TOTO_STATEMENT_TYPE,
16
16
  PASSPORTSIGN_PREDICATE_TYPE,
17
+ PASSPORTSIGN_REVOCATION_PREDICATE_TYPE,
18
+ buildRevocationStatement,
17
19
  buildStatement,
20
+ type BuildRevocationStatementInput,
18
21
  type BuildStatementInput,
19
22
  type DisclosureLevel,
20
23
  type PassportsignPredicate,
24
+ type PassportsignRevocationPredicate,
25
+ type PassportsignRevocationStatement,
21
26
  type PassportsignStatement,
22
27
  } from './statement.js';
23
28
 
24
29
  export {
25
30
  BUNDLE_FORMAT_VERSION,
26
31
  BundleValidationError,
27
- readBundle,
32
+ assembleBundle,
28
33
  validateBundle,
29
- writeBundle,
30
34
  type PassportsignBundle,
31
35
  type RekorBundleFields,
32
36
  } from './bundle.js';
33
37
 
38
+ export { readBundle, writeBundle } from './bundle-fs.js';
39
+
34
40
  export {
35
41
  ERROR_CODES,
36
42
  PassportsignError,
@@ -74,6 +80,12 @@ export {
74
80
  type SignEnvelopeResult,
75
81
  } from './dsse.js';
76
82
 
83
+ export {
84
+ p1363ToDer,
85
+ signEnvelopeWeb,
86
+ type SignEnvelopeWebResult,
87
+ } from './dsse-web.js';
88
+
77
89
  export {
78
90
  DEFAULT_REKOR_BASE_URL,
79
91
  PublicSigstoreRekorClient,
@@ -88,8 +100,15 @@ export {
88
100
  submitBinding,
89
101
  type SubmitBindingDeps,
90
102
  type SubmitBindingResult,
103
+ type SubmittableStatement,
91
104
  } from './submit.js';
92
105
 
106
+ export {
107
+ prepareRevocation,
108
+ type PrepareRevocationInput,
109
+ type PreparedRevocation,
110
+ } from './revoke.js';
111
+
93
112
  export {
94
113
  hashLeaf,
95
114
  hashPair,
@@ -110,6 +129,44 @@ export {
110
129
  type BadgeInput,
111
130
  } from './badge.js';
112
131
 
132
+ export {
133
+ PROFILE_INDEX_FILENAME,
134
+ PROFILE_INDEX_VERSION,
135
+ ProfileIndexValidationError,
136
+ addBinding,
137
+ addRevocation,
138
+ createProfileIndex,
139
+ fetchProfileIndex,
140
+ mergeProfileIndexes,
141
+ profileIndexUrl,
142
+ validateProfileIndex,
143
+ type FetchProfileIndexOptions,
144
+ type ProfileIndex,
145
+ type ProfileIndexBinding,
146
+ type ProfileIndexRevocation,
147
+ } from './profile-index.js';
148
+
149
+ export {
150
+ EntryParseError,
151
+ STALENESS_WINDOW_MS,
152
+ classifyBindings,
153
+ parseIntotoEntry,
154
+ type BindingState,
155
+ type ClassifiedBinding,
156
+ type ClassifyBindingsInput,
157
+ type InTotoStatement,
158
+ type ParsedIntotoEntry,
159
+ } from './classify.js';
160
+
161
+ export {
162
+ lookupBindings,
163
+ lookupFromIndex,
164
+ type LookupBindingsDeps,
165
+ type LookupDeps,
166
+ type LookupEntryProblem,
167
+ type LookupResult,
168
+ } from './lookup.js';
169
+
113
170
  export {
114
171
  verifyBundle,
115
172
  type BundleVerifyResult,