@valkyrianlabs/payload-markdown-docs 0.1.0-canary.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.
- package/README.md +195 -0
- package/dist/admin/DocsSetManager.d.ts +2 -0
- package/dist/admin/DocsSetManager.js +298 -0
- package/dist/admin/DocsSetManager.js.map +1 -0
- package/dist/admin/docsSetManagerData.d.ts +25 -0
- package/dist/admin/docsSetManagerData.js +266 -0
- package/dist/admin/docsSetManagerData.js.map +1 -0
- package/dist/admin/docsSetManagerTypes.d.ts +103 -0
- package/dist/admin/docsSetManagerTypes.js +3 -0
- package/dist/admin/docsSetManagerTypes.js.map +1 -0
- package/dist/admin/index.d.ts +3 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/cli/commands/install.d.ts +2 -0
- package/dist/cli/commands/install.js +211 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/keygen.d.ts +2 -0
- package/dist/cli/commands/keygen.js +89 -0
- package/dist/cli/commands/keygen.js.map +1 -0
- package/dist/cli/commands/manifest.d.ts +2 -0
- package/dist/cli/commands/manifest.js +50 -0
- package/dist/cli/commands/manifest.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +2 -0
- package/dist/cli/commands/plan.js +110 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/push.d.ts +3 -0
- package/dist/cli/commands/push.js +308 -0
- package/dist/cli/commands/push.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +109 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/filesystem.d.ts +20 -0
- package/dist/cli/filesystem.js +96 -0
- package/dist/cli/filesystem.js.map +1 -0
- package/dist/cli/format.d.ts +35 -0
- package/dist/cli/format.js +76 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/http.d.ts +19 -0
- package/dist/cli/http.js +39 -0
- package/dist/cli/http.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +214 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +5 -0
- package/dist/cli/parseArgs.js +219 -0
- package/dist/cli/parseArgs.js.map +1 -0
- package/dist/cli/types.d.ts +51 -0
- package/dist/cli/types.js +3 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/collections/docs.d.ts +9 -0
- package/dist/collections/docs.js +168 -0
- package/dist/collections/docs.js.map +1 -0
- package/dist/collections/docsGroups.d.ts +5 -0
- package/dist/collections/docsGroups.js +57 -0
- package/dist/collections/docsGroups.js.map +1 -0
- package/dist/collections/docsSets.d.ts +8 -0
- package/dist/collections/docsSets.js +158 -0
- package/dist/collections/docsSets.js.map +1 -0
- package/dist/collections/index.d.ts +10 -0
- package/dist/collections/index.js +7 -0
- package/dist/collections/index.js.map +1 -0
- package/dist/collections/nonces.d.ts +6 -0
- package/dist/collections/nonces.js +57 -0
- package/dist/collections/nonces.js.map +1 -0
- package/dist/collections/syncRuns.d.ts +5 -0
- package/dist/collections/syncRuns.js +139 -0
- package/dist/collections/syncRuns.js.map +1 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +23 -0
- package/dist/constants.js.map +1 -0
- package/dist/endpoints/index.d.ts +2 -0
- package/dist/endpoints/index.js +3 -0
- package/dist/endpoints/index.js.map +1 -0
- package/dist/endpoints/sync.d.ts +47 -0
- package/dist/endpoints/sync.js +616 -0
- package/dist/endpoints/sync.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/next/PayloadMarkdownDocsPage.d.ts +7 -0
- package/dist/next/PayloadMarkdownDocsPage.js +142 -0
- package/dist/next/PayloadMarkdownDocsPage.js.map +1 -0
- package/dist/next/index.d.ts +9 -0
- package/dist/next/index.js +7 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/markdown.d.ts +14 -0
- package/dist/next/markdown.js +232 -0
- package/dist/next/markdown.js.map +1 -0
- package/dist/next/metadata.d.ts +3 -0
- package/dist/next/metadata.js +33 -0
- package/dist/next/metadata.js.map +1 -0
- package/dist/next/records.d.ts +14 -0
- package/dist/next/records.js +146 -0
- package/dist/next/records.js.map +1 -0
- package/dist/next/route.d.ts +6 -0
- package/dist/next/route.js +271 -0
- package/dist/next/route.js.map +1 -0
- package/dist/next/sidebar.d.ts +15 -0
- package/dist/next/sidebar.js +137 -0
- package/dist/next/sidebar.js.map +1 -0
- package/dist/next/types.d.ts +117 -0
- package/dist/next/types.js +3 -0
- package/dist/next/types.js.map +1 -0
- package/dist/payload/applyDocsSync.d.ts +54 -0
- package/dist/payload/applyDocsSync.js +176 -0
- package/dist/payload/applyDocsSync.js.map +1 -0
- package/dist/payload/docsConflicts.d.ts +12 -0
- package/dist/payload/docsConflicts.js +34 -0
- package/dist/payload/docsConflicts.js.map +1 -0
- package/dist/payload/docsData.d.ts +23 -0
- package/dist/payload/docsData.js +59 -0
- package/dist/payload/docsData.js.map +1 -0
- package/dist/payload/docsSets.d.ts +38 -0
- package/dist/payload/docsSets.js +57 -0
- package/dist/payload/docsSets.js.map +1 -0
- package/dist/payload/existingDocs.d.ts +43 -0
- package/dist/payload/existingDocs.js +97 -0
- package/dist/payload/existingDocs.js.map +1 -0
- package/dist/payload/index.d.ts +15 -0
- package/dist/payload/index.js +10 -0
- package/dist/payload/index.js.map +1 -0
- package/dist/payload/routeCollisions.d.ts +31 -0
- package/dist/payload/routeCollisions.js +104 -0
- package/dist/payload/routeCollisions.js.map +1 -0
- package/dist/payload/syncRuns.d.ts +60 -0
- package/dist/payload/syncRuns.js +53 -0
- package/dist/payload/syncRuns.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +165 -0
- package/dist/plugin.js.map +1 -0
- package/dist/routing/index.d.ts +3 -0
- package/dist/routing/index.js +4 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/routing/paths.d.ts +7 -0
- package/dist/routing/paths.js +23 -0
- package/dist/routing/paths.js.map +1 -0
- package/dist/routing/reservations.d.ts +37 -0
- package/dist/routing/reservations.js +79 -0
- package/dist/routing/reservations.js.map +1 -0
- package/dist/security/canonical.d.ts +12 -0
- package/dist/security/canonical.js +24 -0
- package/dist/security/canonical.js.map +1 -0
- package/dist/security/githubOidc.d.ts +45 -0
- package/dist/security/githubOidc.js +177 -0
- package/dist/security/githubOidc.js.map +1 -0
- package/dist/security/headers.d.ts +22 -0
- package/dist/security/headers.js +44 -0
- package/dist/security/headers.js.map +1 -0
- package/dist/security/index.d.ts +15 -0
- package/dist/security/index.js +9 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/jwks.d.ts +20 -0
- package/dist/security/jwks.js +40 -0
- package/dist/security/jwks.js.map +1 -0
- package/dist/security/jwt.d.ts +10 -0
- package/dist/security/jwt.js +42 -0
- package/dist/security/jwt.js.map +1 -0
- package/dist/security/nonce.d.ts +34 -0
- package/dist/security/nonce.js +43 -0
- package/dist/security/nonce.js.map +1 -0
- package/dist/security/sign.d.ts +13 -0
- package/dist/security/sign.js +39 -0
- package/dist/security/sign.js.map +1 -0
- package/dist/security/verify.d.ts +28 -0
- package/dist/security/verify.js +54 -0
- package/dist/security/verify.js.map +1 -0
- package/dist/skills/codex/SKILL.md +173 -0
- package/dist/skills/codex/examples/docs-page.md +42 -0
- package/dist/skills/codex/examples/github-actions.md +64 -0
- package/dist/skills/codex/reference/admin.md +28 -0
- package/dist/skills/codex/reference/frontmatter.md +39 -0
- package/dist/skills/codex/reference/payload-markdown-directives.md +77 -0
- package/dist/skills/codex/reference/routing.md +35 -0
- package/dist/skills/codex/reference/sync.md +35 -0
- package/dist/skills/codex/reference/troubleshooting.md +53 -0
- package/dist/skills/codex/reference/workflow.md +39 -0
- package/dist/sync/aiExportManifest.d.ts +58 -0
- package/dist/sync/aiExportManifest.js +430 -0
- package/dist/sync/aiExportManifest.js.map +1 -0
- package/dist/sync/frontmatter.d.ts +28 -0
- package/dist/sync/frontmatter.js +210 -0
- package/dist/sync/frontmatter.js.map +1 -0
- package/dist/sync/hash.d.ts +1 -0
- package/dist/sync/hash.js +8 -0
- package/dist/sync/hash.js.map +1 -0
- package/dist/sync/index.d.ts +12 -0
- package/dist/sync/index.js +9 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/manifest.d.ts +58 -0
- package/dist/sync/manifest.js +21 -0
- package/dist/sync/manifest.js.map +1 -0
- package/dist/sync/paths.d.ts +16 -0
- package/dist/sync/paths.js +116 -0
- package/dist/sync/paths.js.map +1 -0
- package/dist/sync/plan.d.ts +29 -0
- package/dist/sync/plan.js +72 -0
- package/dist/sync/plan.js.map +1 -0
- package/dist/sync/validate.d.ts +26 -0
- package/dist/sync/validate.js +308 -0
- package/dist/sync/validate.js.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +143 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
import { DEFAULT_GITHUB_OIDC_ISSUER, DEFAULT_MAX_SKEW_SECONDS } from '../constants.js';
|
|
3
|
+
import { fetchJwks, findJwkByKid, getGithubOidcJwksUrl } from './jwks.js';
|
|
4
|
+
import { decodeJwt } from './jwt.js';
|
|
5
|
+
const isString = (value)=>typeof value === 'string' && value.trim() !== '';
|
|
6
|
+
const isStringArray = (value)=>Array.isArray(value) && value.every(isString);
|
|
7
|
+
const isNumber = (value)=>typeof value === 'number' && Number.isFinite(value);
|
|
8
|
+
const getStringClaim = (payload, claim)=>{
|
|
9
|
+
const value = payload[claim];
|
|
10
|
+
return isString(value) ? value : undefined;
|
|
11
|
+
};
|
|
12
|
+
const getNumberClaim = (payload, claim)=>{
|
|
13
|
+
const value = payload[claim];
|
|
14
|
+
return isNumber(value) ? value : undefined;
|
|
15
|
+
};
|
|
16
|
+
const getAudienceClaim = (payload)=>{
|
|
17
|
+
const value = payload.aud;
|
|
18
|
+
if (isString(value) || isStringArray(value)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
};
|
|
23
|
+
const toClaims = (payload)=>{
|
|
24
|
+
const aud = getAudienceClaim(payload);
|
|
25
|
+
const exp = getNumberClaim(payload, 'exp');
|
|
26
|
+
const iat = getNumberClaim(payload, 'iat');
|
|
27
|
+
const iss = getStringClaim(payload, 'iss');
|
|
28
|
+
const jti = getStringClaim(payload, 'jti');
|
|
29
|
+
const ref = getStringClaim(payload, 'ref');
|
|
30
|
+
const repository = getStringClaim(payload, 'repository');
|
|
31
|
+
const repositoryOwner = getStringClaim(payload, 'repository_owner');
|
|
32
|
+
const sub = getStringClaim(payload, 'sub');
|
|
33
|
+
if (!aud || exp === undefined || iat === undefined || !iss || !jti || !ref || !repository || !repositoryOwner || !sub) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
actor: getStringClaim(payload, 'actor'),
|
|
38
|
+
aud,
|
|
39
|
+
environment: getStringClaim(payload, 'environment'),
|
|
40
|
+
event_name: getStringClaim(payload, 'event_name'),
|
|
41
|
+
exp,
|
|
42
|
+
iat,
|
|
43
|
+
iss,
|
|
44
|
+
job_workflow_ref: getStringClaim(payload, 'job_workflow_ref'),
|
|
45
|
+
jti,
|
|
46
|
+
nbf: getNumberClaim(payload, 'nbf'),
|
|
47
|
+
ref,
|
|
48
|
+
repository,
|
|
49
|
+
repository_owner: repositoryOwner,
|
|
50
|
+
sha: getStringClaim(payload, 'sha'),
|
|
51
|
+
sub,
|
|
52
|
+
workflow: getStringClaim(payload, 'workflow'),
|
|
53
|
+
workflow_ref: getStringClaim(payload, 'workflow_ref')
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const issue = (code, message)=>({
|
|
57
|
+
code,
|
|
58
|
+
message,
|
|
59
|
+
ok: false
|
|
60
|
+
});
|
|
61
|
+
const includesIfConfigured = (allowed, value)=>{
|
|
62
|
+
if (!allowed || allowed.length === 0) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return value !== undefined && allowed.includes(value);
|
|
66
|
+
};
|
|
67
|
+
const audienceMatches = (audience, expected)=>Array.isArray(audience) ? audience.includes(expected) : audience === expected;
|
|
68
|
+
const verifyJwtSignature = ({ jwk, signature, signingInput })=>{
|
|
69
|
+
try {
|
|
70
|
+
const publicKey = createPublicKey({
|
|
71
|
+
format: 'jwk',
|
|
72
|
+
key: jwk
|
|
73
|
+
});
|
|
74
|
+
return verify('RSA-SHA256', Buffer.from(signingInput, 'utf8'), publicKey, signature);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
export const verifyGitHubOidcToken = async ({ config, fetchJson, now = new Date(), token })=>{
|
|
80
|
+
const decoded = decodeJwt(token);
|
|
81
|
+
if (!decoded) {
|
|
82
|
+
return issue('oidc_invalid_token', 'GitHub OIDC token is malformed.');
|
|
83
|
+
}
|
|
84
|
+
if (decoded.header.alg !== 'RS256') {
|
|
85
|
+
return issue('oidc_invalid_token', 'GitHub OIDC token must use RS256.');
|
|
86
|
+
}
|
|
87
|
+
if (!isString(decoded.header.kid)) {
|
|
88
|
+
return issue('oidc_invalid_token', 'GitHub OIDC token is missing kid.');
|
|
89
|
+
}
|
|
90
|
+
const issuer = config.issuer ?? DEFAULT_GITHUB_OIDC_ISSUER;
|
|
91
|
+
let jwksUrl;
|
|
92
|
+
try {
|
|
93
|
+
jwksUrl = await getGithubOidcJwksUrl({
|
|
94
|
+
fetchJson,
|
|
95
|
+
issuer,
|
|
96
|
+
jwksUrl: config.jwksUrl
|
|
97
|
+
});
|
|
98
|
+
const jwks = await fetchJwks({
|
|
99
|
+
fetchJson,
|
|
100
|
+
now,
|
|
101
|
+
url: jwksUrl
|
|
102
|
+
});
|
|
103
|
+
const jwk = findJwkByKid({
|
|
104
|
+
jwks,
|
|
105
|
+
kid: decoded.header.kid
|
|
106
|
+
});
|
|
107
|
+
if (!jwk || !verifyJwtSignature({
|
|
108
|
+
jwk,
|
|
109
|
+
signature: decoded.signature,
|
|
110
|
+
signingInput: decoded.signingInput
|
|
111
|
+
})) {
|
|
112
|
+
return issue('oidc_invalid_token', 'GitHub OIDC token signature is invalid.');
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
return issue('oidc_jwks_unavailable', 'GitHub OIDC signing keys are unavailable.');
|
|
116
|
+
}
|
|
117
|
+
if (!isString(decoded.payload.jti)) {
|
|
118
|
+
return issue('oidc_missing_jti', 'GitHub OIDC token is missing jti.');
|
|
119
|
+
}
|
|
120
|
+
const claims = toClaims(decoded.payload);
|
|
121
|
+
if (!claims) {
|
|
122
|
+
return issue('oidc_missing_claim', 'GitHub OIDC token is missing a required claim.');
|
|
123
|
+
}
|
|
124
|
+
if (claims.iss !== issuer) {
|
|
125
|
+
return issue('oidc_invalid_issuer', 'GitHub OIDC token issuer is not allowed.');
|
|
126
|
+
}
|
|
127
|
+
if (!audienceMatches(claims.aud, config.audience)) {
|
|
128
|
+
return issue('oidc_invalid_audience', 'GitHub OIDC token audience is not allowed.');
|
|
129
|
+
}
|
|
130
|
+
const maxSkewSeconds = config.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS;
|
|
131
|
+
const nowSeconds = now.getTime() / 1000;
|
|
132
|
+
if (claims.exp + maxSkewSeconds < nowSeconds) {
|
|
133
|
+
return issue('oidc_expired', 'GitHub OIDC token has expired.');
|
|
134
|
+
}
|
|
135
|
+
if (claims.nbf !== undefined && claims.nbf - maxSkewSeconds > nowSeconds) {
|
|
136
|
+
return issue('oidc_not_yet_valid', 'GitHub OIDC token is not valid yet.');
|
|
137
|
+
}
|
|
138
|
+
if (claims.iat - maxSkewSeconds > nowSeconds) {
|
|
139
|
+
return issue('oidc_not_yet_valid', 'GitHub OIDC token was issued in the future.');
|
|
140
|
+
}
|
|
141
|
+
const hasRepositoryAllowlist = (config.allowedRepositories?.length ?? 0) > 0 || (config.allowedRepositoryOwners?.length ?? 0) > 0;
|
|
142
|
+
if (!hasRepositoryAllowlist) {
|
|
143
|
+
return issue('oidc_repository_not_allowed', 'GitHub OIDC auth requires an allowed repository or repository owner.');
|
|
144
|
+
}
|
|
145
|
+
if (!includesIfConfigured(config.allowedRepositories, claims.repository)) {
|
|
146
|
+
return issue('oidc_repository_not_allowed', 'GitHub OIDC token repository is not allowed.');
|
|
147
|
+
}
|
|
148
|
+
if (!includesIfConfigured(config.allowedRepositoryOwners, claims.repository_owner)) {
|
|
149
|
+
return issue('oidc_owner_not_allowed', 'GitHub OIDC token repository owner is not allowed.');
|
|
150
|
+
}
|
|
151
|
+
if (!includesIfConfigured(config.allowedRefs, claims.ref)) {
|
|
152
|
+
return issue('oidc_ref_not_allowed', 'GitHub OIDC token ref is not allowed.');
|
|
153
|
+
}
|
|
154
|
+
if (!includesIfConfigured(config.allowedWorkflows, claims.workflow)) {
|
|
155
|
+
return issue('oidc_workflow_not_allowed', 'GitHub OIDC token workflow is not allowed.');
|
|
156
|
+
}
|
|
157
|
+
const workflowRef = claims.workflow_ref ?? claims.job_workflow_ref;
|
|
158
|
+
if (!includesIfConfigured(config.allowedWorkflowRefs, workflowRef)) {
|
|
159
|
+
return issue('oidc_workflow_not_allowed', 'GitHub OIDC token workflow ref is not allowed.');
|
|
160
|
+
}
|
|
161
|
+
if (!includesIfConfigured(config.allowedEnvironments, claims.environment)) {
|
|
162
|
+
return issue('oidc_environment_not_allowed', 'GitHub OIDC token environment is not allowed.');
|
|
163
|
+
}
|
|
164
|
+
if (claims.event_name === 'pull_request' && config.allowPullRequests !== true) {
|
|
165
|
+
return issue('oidc_pull_request_not_allowed', 'GitHub OIDC pull request events are not allowed.');
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
token: {
|
|
170
|
+
claims,
|
|
171
|
+
expiresAt: new Date(claims.exp * 1000),
|
|
172
|
+
keyId: `github-oidc:${claims.repository}`
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
//# sourceMappingURL=githubOidc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/githubOidc.ts"],"sourcesContent":["import {\n createPublicKey,\n type JsonWebKey,\n verify,\n} from 'node:crypto'\n\nimport type { PayloadMarkdownDocsAuthConfig } from '../types.js'\nimport type { FetchJson } from './jwks.js'\n\nimport {\n DEFAULT_GITHUB_OIDC_ISSUER,\n DEFAULT_MAX_SKEW_SECONDS,\n} from '../constants.js'\nimport {\n fetchJwks,\n findJwkByKid,\n getGithubOidcJwksUrl,\n} from './jwks.js'\nimport { decodeJwt } from './jwt.js'\n\nexport type GitHubOidcErrorCode =\n | 'oidc_environment_not_allowed'\n | 'oidc_expired'\n | 'oidc_invalid_audience'\n | 'oidc_invalid_issuer'\n | 'oidc_invalid_token'\n | 'oidc_jwks_unavailable'\n | 'oidc_missing_claim'\n | 'oidc_missing_jti'\n | 'oidc_not_yet_valid'\n | 'oidc_owner_not_allowed'\n | 'oidc_pull_request_not_allowed'\n | 'oidc_ref_not_allowed'\n | 'oidc_repository_not_allowed'\n | 'oidc_workflow_not_allowed'\n\nexport type GitHubOidcClaims = {\n actor?: string\n aud: string | string[]\n environment?: string\n event_name?: string\n exp: number\n iat: number\n iss: string\n job_workflow_ref?: string\n jti: string\n nbf?: number\n ref: string\n repository: string\n repository_owner: string\n sha?: string\n sub: string\n workflow?: string\n workflow_ref?: string\n}\n\nexport type VerifiedGitHubOidcToken = {\n claims: GitHubOidcClaims\n expiresAt: Date\n keyId: string\n}\n\nexport type VerifyGitHubOidcTokenResult =\n | {\n code: GitHubOidcErrorCode\n message: string\n ok: false\n }\n | {\n ok: true\n token: VerifiedGitHubOidcToken\n }\n\ntype GitHubOidcAuthConfig = Extract<\n PayloadMarkdownDocsAuthConfig,\n { mode: 'github-oidc' }\n>\n\nconst isString = (value: unknown): value is string =>\n typeof value === 'string' && value.trim() !== ''\n\nconst isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every(isString)\n\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value)\n\nconst getStringClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): string | undefined => {\n const value = payload[claim]\n\n return isString(value) ? value : undefined\n}\n\nconst getNumberClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): number | undefined => {\n const value = payload[claim]\n\n return isNumber(value) ? value : undefined\n}\n\nconst getAudienceClaim = (\n payload: Record<string, unknown>,\n): string | string[] | undefined => {\n const value = payload.aud\n\n if (isString(value) || isStringArray(value)) {\n return value\n }\n\n return undefined\n}\n\nconst toClaims = (\n payload: Record<string, unknown>,\n): GitHubOidcClaims | undefined => {\n const aud = getAudienceClaim(payload)\n const exp = getNumberClaim(payload, 'exp')\n const iat = getNumberClaim(payload, 'iat')\n const iss = getStringClaim(payload, 'iss')\n const jti = getStringClaim(payload, 'jti')\n const ref = getStringClaim(payload, 'ref')\n const repository = getStringClaim(payload, 'repository')\n const repositoryOwner = getStringClaim(payload, 'repository_owner')\n const sub = getStringClaim(payload, 'sub')\n\n if (\n !aud ||\n exp === undefined ||\n iat === undefined ||\n !iss ||\n !jti ||\n !ref ||\n !repository ||\n !repositoryOwner ||\n !sub\n ) {\n return undefined\n }\n\n return {\n actor: getStringClaim(payload, 'actor'),\n aud,\n environment: getStringClaim(payload, 'environment'),\n event_name: getStringClaim(payload, 'event_name'),\n exp,\n iat,\n iss,\n job_workflow_ref: getStringClaim(payload, 'job_workflow_ref'),\n jti,\n nbf: getNumberClaim(payload, 'nbf'),\n ref,\n repository,\n repository_owner: repositoryOwner,\n sha: getStringClaim(payload, 'sha'),\n sub,\n workflow: getStringClaim(payload, 'workflow'),\n workflow_ref: getStringClaim(payload, 'workflow_ref'),\n }\n}\n\nconst issue = (\n code: GitHubOidcErrorCode,\n message: string,\n): VerifyGitHubOidcTokenResult => ({\n code,\n message,\n ok: false,\n})\n\nconst includesIfConfigured = (\n allowed: string[] | undefined,\n value: string | undefined,\n): boolean => {\n if (!allowed || allowed.length === 0) {\n return true\n }\n\n return value !== undefined && allowed.includes(value)\n}\n\nconst audienceMatches = (\n audience: string | string[],\n expected: string,\n): boolean =>\n Array.isArray(audience) ? audience.includes(expected) : audience === expected\n\nconst verifyJwtSignature = ({\n jwk,\n signature,\n signingInput,\n}: {\n jwk: Record<string, unknown>\n signature: Buffer\n signingInput: string\n}): boolean => {\n try {\n const publicKey = createPublicKey({\n format: 'jwk',\n key: jwk as JsonWebKey,\n })\n\n return verify(\n 'RSA-SHA256',\n Buffer.from(signingInput, 'utf8'),\n publicKey,\n signature,\n )\n } catch {\n return false\n }\n}\n\nexport const verifyGitHubOidcToken = async ({\n config,\n fetchJson,\n now = new Date(),\n token,\n}: {\n config: GitHubOidcAuthConfig\n fetchJson?: FetchJson\n now?: Date\n token: string\n}): Promise<VerifyGitHubOidcTokenResult> => {\n const decoded = decodeJwt(token)\n\n if (!decoded) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is malformed.')\n }\n\n if (decoded.header.alg !== 'RS256') {\n return issue('oidc_invalid_token', 'GitHub OIDC token must use RS256.')\n }\n\n if (!isString(decoded.header.kid)) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is missing kid.')\n }\n\n const issuer = config.issuer ?? DEFAULT_GITHUB_OIDC_ISSUER\n let jwksUrl: string\n\n try {\n jwksUrl = await getGithubOidcJwksUrl({\n fetchJson,\n issuer,\n jwksUrl: config.jwksUrl,\n })\n const jwks = await fetchJwks({\n fetchJson,\n now,\n url: jwksUrl,\n })\n const jwk = findJwkByKid({\n jwks,\n kid: decoded.header.kid,\n })\n\n if (\n !jwk ||\n !verifyJwtSignature({\n jwk,\n signature: decoded.signature,\n signingInput: decoded.signingInput,\n })\n ) {\n return issue('oidc_invalid_token', 'GitHub OIDC token signature is invalid.')\n }\n } catch {\n return issue('oidc_jwks_unavailable', 'GitHub OIDC signing keys are unavailable.')\n }\n\n if (!isString(decoded.payload.jti)) {\n return issue('oidc_missing_jti', 'GitHub OIDC token is missing jti.')\n }\n\n const claims = toClaims(decoded.payload)\n\n if (!claims) {\n return issue('oidc_missing_claim', 'GitHub OIDC token is missing a required claim.')\n }\n\n if (claims.iss !== issuer) {\n return issue('oidc_invalid_issuer', 'GitHub OIDC token issuer is not allowed.')\n }\n\n if (!audienceMatches(claims.aud, config.audience)) {\n return issue('oidc_invalid_audience', 'GitHub OIDC token audience is not allowed.')\n }\n\n const maxSkewSeconds = config.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS\n const nowSeconds = now.getTime() / 1000\n\n if (claims.exp + maxSkewSeconds < nowSeconds) {\n return issue('oidc_expired', 'GitHub OIDC token has expired.')\n }\n\n if (claims.nbf !== undefined && claims.nbf - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token is not valid yet.')\n }\n\n if (claims.iat - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token was issued in the future.')\n }\n\n const hasRepositoryAllowlist =\n (config.allowedRepositories?.length ?? 0) > 0 ||\n (config.allowedRepositoryOwners?.length ?? 0) > 0\n\n if (!hasRepositoryAllowlist) {\n return issue(\n 'oidc_repository_not_allowed',\n 'GitHub OIDC auth requires an allowed repository or repository owner.',\n )\n }\n\n if (!includesIfConfigured(config.allowedRepositories, claims.repository)) {\n return issue(\n 'oidc_repository_not_allowed',\n 'GitHub OIDC token repository is not allowed.',\n )\n }\n\n if (!includesIfConfigured(config.allowedRepositoryOwners, claims.repository_owner)) {\n return issue(\n 'oidc_owner_not_allowed',\n 'GitHub OIDC token repository owner is not allowed.',\n )\n }\n\n if (!includesIfConfigured(config.allowedRefs, claims.ref)) {\n return issue('oidc_ref_not_allowed', 'GitHub OIDC token ref is not allowed.')\n }\n\n if (!includesIfConfigured(config.allowedWorkflows, claims.workflow)) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'GitHub OIDC token workflow is not allowed.',\n )\n }\n\n const workflowRef = claims.workflow_ref ?? claims.job_workflow_ref\n\n if (!includesIfConfigured(config.allowedWorkflowRefs, workflowRef)) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'GitHub OIDC token workflow ref is not allowed.',\n )\n }\n\n if (!includesIfConfigured(config.allowedEnvironments, claims.environment)) {\n return issue(\n 'oidc_environment_not_allowed',\n 'GitHub OIDC token environment is not allowed.',\n )\n }\n\n if (claims.event_name === 'pull_request' && config.allowPullRequests !== true) {\n return issue(\n 'oidc_pull_request_not_allowed',\n 'GitHub OIDC pull request events are not allowed.',\n )\n }\n\n return {\n ok: true,\n token: {\n claims,\n expiresAt: new Date(claims.exp * 1000),\n keyId: `github-oidc:${claims.repository}`,\n },\n }\n}\n"],"names":["createPublicKey","verify","DEFAULT_GITHUB_OIDC_ISSUER","DEFAULT_MAX_SKEW_SECONDS","fetchJwks","findJwkByKid","getGithubOidcJwksUrl","decodeJwt","isString","value","trim","isStringArray","Array","isArray","every","isNumber","Number","isFinite","getStringClaim","payload","claim","undefined","getNumberClaim","getAudienceClaim","aud","toClaims","exp","iat","iss","jti","ref","repository","repositoryOwner","sub","actor","environment","event_name","job_workflow_ref","nbf","repository_owner","sha","workflow","workflow_ref","issue","code","message","ok","includesIfConfigured","allowed","length","includes","audienceMatches","audience","expected","verifyJwtSignature","jwk","signature","signingInput","publicKey","format","key","Buffer","from","verifyGitHubOidcToken","config","fetchJson","now","Date","token","decoded","header","alg","kid","issuer","jwksUrl","jwks","url","claims","maxSkewSeconds","nowSeconds","getTime","hasRepositoryAllowlist","allowedRepositories","allowedRepositoryOwners","allowedRefs","allowedWorkflows","workflowRef","allowedWorkflowRefs","allowedEnvironments","allowPullRequests","expiresAt","keyId"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const syncHeaderNames: {
|
|
2
|
+
readonly bodySha256: "x-vl-md-docs-body-sha256";
|
|
3
|
+
readonly keyId: "x-vl-md-docs-key-id";
|
|
4
|
+
readonly nonce: "x-vl-md-docs-nonce";
|
|
5
|
+
readonly signature: "x-vl-md-docs-signature";
|
|
6
|
+
readonly timestamp: "x-vl-md-docs-timestamp";
|
|
7
|
+
};
|
|
8
|
+
export type SyncRequestHeaders = {
|
|
9
|
+
bodySha256: string;
|
|
10
|
+
keyId: string;
|
|
11
|
+
nonce: string;
|
|
12
|
+
signature: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
};
|
|
15
|
+
export type ExtractSyncHeadersResult = {
|
|
16
|
+
header: string;
|
|
17
|
+
ok: false;
|
|
18
|
+
} | {
|
|
19
|
+
headers: SyncRequestHeaders;
|
|
20
|
+
ok: true;
|
|
21
|
+
};
|
|
22
|
+
export declare const extractSyncRequestHeaders: (headers: Headers) => ExtractSyncHeadersResult;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const syncHeaderNames = {
|
|
2
|
+
bodySha256: 'x-vl-md-docs-body-sha256',
|
|
3
|
+
keyId: 'x-vl-md-docs-key-id',
|
|
4
|
+
nonce: 'x-vl-md-docs-nonce',
|
|
5
|
+
signature: 'x-vl-md-docs-signature',
|
|
6
|
+
timestamp: 'x-vl-md-docs-timestamp'
|
|
7
|
+
};
|
|
8
|
+
const displayHeaderNames = {
|
|
9
|
+
bodySha256: 'X-VL-MD-DOCS-Body-SHA256',
|
|
10
|
+
keyId: 'X-VL-MD-DOCS-Key-Id',
|
|
11
|
+
nonce: 'X-VL-MD-DOCS-Nonce',
|
|
12
|
+
signature: 'X-VL-MD-DOCS-Signature',
|
|
13
|
+
timestamp: 'X-VL-MD-DOCS-Timestamp'
|
|
14
|
+
};
|
|
15
|
+
export const extractSyncRequestHeaders = (headers)=>{
|
|
16
|
+
const extracted = {
|
|
17
|
+
bodySha256: headers.get(syncHeaderNames.bodySha256) ?? undefined,
|
|
18
|
+
keyId: headers.get(syncHeaderNames.keyId) ?? undefined,
|
|
19
|
+
nonce: headers.get(syncHeaderNames.nonce) ?? undefined,
|
|
20
|
+
signature: headers.get(syncHeaderNames.signature) ?? undefined,
|
|
21
|
+
timestamp: headers.get(syncHeaderNames.timestamp) ?? undefined
|
|
22
|
+
};
|
|
23
|
+
for (const key of Object.keys(extracted)){
|
|
24
|
+
const value = extracted[key];
|
|
25
|
+
if (!value || value.trim() === '') {
|
|
26
|
+
return {
|
|
27
|
+
header: displayHeaderNames[key],
|
|
28
|
+
ok: false
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
headers: {
|
|
34
|
+
bodySha256: extracted.bodySha256?.trim() ?? '',
|
|
35
|
+
keyId: extracted.keyId?.trim() ?? '',
|
|
36
|
+
nonce: extracted.nonce?.trim() ?? '',
|
|
37
|
+
signature: extracted.signature?.trim() ?? '',
|
|
38
|
+
timestamp: extracted.timestamp?.trim() ?? ''
|
|
39
|
+
},
|
|
40
|
+
ok: true
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/headers.ts"],"sourcesContent":["export const syncHeaderNames = {\n bodySha256: 'x-vl-md-docs-body-sha256',\n keyId: 'x-vl-md-docs-key-id',\n nonce: 'x-vl-md-docs-nonce',\n signature: 'x-vl-md-docs-signature',\n timestamp: 'x-vl-md-docs-timestamp',\n} as const\n\nexport type SyncRequestHeaders = {\n bodySha256: string\n keyId: string\n nonce: string\n signature: string\n timestamp: string\n}\n\nexport type ExtractSyncHeadersResult =\n | {\n header: string\n ok: false\n }\n | {\n headers: SyncRequestHeaders\n ok: true\n }\n\nconst displayHeaderNames: Record<keyof SyncRequestHeaders, string> = {\n bodySha256: 'X-VL-MD-DOCS-Body-SHA256',\n keyId: 'X-VL-MD-DOCS-Key-Id',\n nonce: 'X-VL-MD-DOCS-Nonce',\n signature: 'X-VL-MD-DOCS-Signature',\n timestamp: 'X-VL-MD-DOCS-Timestamp',\n}\n\nexport const extractSyncRequestHeaders = (\n headers: Headers,\n): ExtractSyncHeadersResult => {\n const extracted: Record<keyof SyncRequestHeaders, string | undefined> = {\n bodySha256: headers.get(syncHeaderNames.bodySha256) ?? undefined,\n keyId: headers.get(syncHeaderNames.keyId) ?? undefined,\n nonce: headers.get(syncHeaderNames.nonce) ?? undefined,\n signature: headers.get(syncHeaderNames.signature) ?? undefined,\n timestamp: headers.get(syncHeaderNames.timestamp) ?? undefined,\n }\n\n for (const key of Object.keys(extracted) as Array<keyof SyncRequestHeaders>) {\n const value = extracted[key]\n\n if (!value || value.trim() === '') {\n return {\n header: displayHeaderNames[key],\n ok: false,\n }\n }\n }\n\n return {\n headers: {\n bodySha256: extracted.bodySha256?.trim() ?? '',\n keyId: extracted.keyId?.trim() ?? '',\n nonce: extracted.nonce?.trim() ?? '',\n signature: extracted.signature?.trim() ?? '',\n timestamp: extracted.timestamp?.trim() ?? '',\n },\n ok: true,\n }\n}\n"],"names":["syncHeaderNames","bodySha256","keyId","nonce","signature","timestamp","displayHeaderNames","extractSyncRequestHeaders","headers","extracted","get","undefined","key","Object","keys","value","trim","header","ok"],"mappings":"AAAA,OAAO,MAAMA,kBAAkB;IAC7BC,YAAY;IACZC,OAAO;IACPC,OAAO;IACPC,WAAW;IACXC,WAAW;AACb,EAAU;AAoBV,MAAMC,qBAA+D;IACnEL,YAAY;IACZC,OAAO;IACPC,OAAO;IACPC,WAAW;IACXC,WAAW;AACb;AAEA,OAAO,MAAME,4BAA4B,CACvCC;IAEA,MAAMC,YAAkE;QACtER,YAAYO,QAAQE,GAAG,CAACV,gBAAgBC,UAAU,KAAKU;QACvDT,OAAOM,QAAQE,GAAG,CAACV,gBAAgBE,KAAK,KAAKS;QAC7CR,OAAOK,QAAQE,GAAG,CAACV,gBAAgBG,KAAK,KAAKQ;QAC7CP,WAAWI,QAAQE,GAAG,CAACV,gBAAgBI,SAAS,KAAKO;QACrDN,WAAWG,QAAQE,GAAG,CAACV,gBAAgBK,SAAS,KAAKM;IACvD;IAEA,KAAK,MAAMC,OAAOC,OAAOC,IAAI,CAACL,WAA+C;QAC3E,MAAMM,QAAQN,SAAS,CAACG,IAAI;QAE5B,IAAI,CAACG,SAASA,MAAMC,IAAI,OAAO,IAAI;YACjC,OAAO;gBACLC,QAAQX,kBAAkB,CAACM,IAAI;gBAC/BM,IAAI;YACN;QACF;IACF;IAEA,OAAO;QACLV,SAAS;YACPP,YAAYQ,UAAUR,UAAU,EAAEe,UAAU;YAC5Cd,OAAOO,UAAUP,KAAK,EAAEc,UAAU;YAClCb,OAAOM,UAAUN,KAAK,EAAEa,UAAU;YAClCZ,WAAWK,UAAUL,SAAS,EAAEY,UAAU;YAC1CX,WAAWI,UAAUJ,SAAS,EAAEW,UAAU;QAC5C;QACAE,IAAI;IACN;AACF,EAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { buildCanonicalSigningString, getCanonicalPathFromRequestUrl, } from './canonical.js';
|
|
2
|
+
export type { CanonicalSigningStringInput } from './canonical.js';
|
|
3
|
+
export { verifyGitHubOidcToken } from './githubOidc.js';
|
|
4
|
+
export type { GitHubOidcClaims, GitHubOidcErrorCode, VerifiedGitHubOidcToken, VerifyGitHubOidcTokenResult, } from './githubOidc.js';
|
|
5
|
+
export { extractSyncRequestHeaders, syncHeaderNames } from './headers.js';
|
|
6
|
+
export type { ExtractSyncHeadersResult, SyncRequestHeaders, } from './headers.js';
|
|
7
|
+
export type { FetchJson } from './jwks.js';
|
|
8
|
+
export { decodeJwt, toBase64Url, } from './jwt.js';
|
|
9
|
+
export type { DecodedJwt } from './jwt.js';
|
|
10
|
+
export { assertNonceNotReplayed, storeAcceptedNonce, } from './nonce.js';
|
|
11
|
+
export type { NoncePayloadOperations } from './nonce.js';
|
|
12
|
+
export { signDocsSyncRequest } from './sign.js';
|
|
13
|
+
export type { SignDocsSyncRequestOptions, SignedDocsSyncRequest, } from './sign.js';
|
|
14
|
+
export { validateTimestampSkew, verifyBodySha256, verifyEd25519Signature, } from './verify.js';
|
|
15
|
+
export type { ValidateTimestampResult, VerifyBodyHashResult, } from './verify.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { buildCanonicalSigningString, getCanonicalPathFromRequestUrl } from './canonical.js';
|
|
2
|
+
export { verifyGitHubOidcToken } from './githubOidc.js';
|
|
3
|
+
export { extractSyncRequestHeaders, syncHeaderNames } from './headers.js';
|
|
4
|
+
export { decodeJwt, toBase64Url } from './jwt.js';
|
|
5
|
+
export { assertNonceNotReplayed, storeAcceptedNonce } from './nonce.js';
|
|
6
|
+
export { signDocsSyncRequest } from './sign.js';
|
|
7
|
+
export { validateTimestampSkew, verifyBodySha256, verifyEd25519Signature } from './verify.js';
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/index.ts"],"sourcesContent":["export {\n buildCanonicalSigningString,\n getCanonicalPathFromRequestUrl,\n} from './canonical.js'\nexport type { CanonicalSigningStringInput } from './canonical.js'\nexport { verifyGitHubOidcToken } from './githubOidc.js'\nexport type {\n GitHubOidcClaims,\n GitHubOidcErrorCode,\n VerifiedGitHubOidcToken,\n VerifyGitHubOidcTokenResult,\n} from './githubOidc.js'\nexport { extractSyncRequestHeaders, syncHeaderNames } from './headers.js'\nexport type {\n ExtractSyncHeadersResult,\n SyncRequestHeaders,\n} from './headers.js'\nexport type { FetchJson } from './jwks.js'\nexport {\n decodeJwt,\n toBase64Url,\n} from './jwt.js'\nexport type { DecodedJwt } from './jwt.js'\nexport {\n assertNonceNotReplayed,\n storeAcceptedNonce,\n} from './nonce.js'\nexport type { NoncePayloadOperations } from './nonce.js'\nexport { signDocsSyncRequest } from './sign.js'\nexport type {\n SignDocsSyncRequestOptions,\n SignedDocsSyncRequest,\n} from './sign.js'\nexport {\n validateTimestampSkew,\n verifyBodySha256,\n verifyEd25519Signature,\n} from './verify.js'\nexport type {\n ValidateTimestampResult,\n VerifyBodyHashResult,\n} from './verify.js'\n"],"names":["buildCanonicalSigningString","getCanonicalPathFromRequestUrl","verifyGitHubOidcToken","extractSyncRequestHeaders","syncHeaderNames","decodeJwt","toBase64Url","assertNonceNotReplayed","storeAcceptedNonce","signDocsSyncRequest","validateTimestampSkew","verifyBodySha256","verifyEd25519Signature"],"mappings":"AAAA,SACEA,2BAA2B,EAC3BC,8BAA8B,QACzB,iBAAgB;AAEvB,SAASC,qBAAqB,QAAQ,kBAAiB;AAOvD,SAASC,yBAAyB,EAAEC,eAAe,QAAQ,eAAc;AAMzE,SACEC,SAAS,EACTC,WAAW,QACN,WAAU;AAEjB,SACEC,sBAAsB,EACtBC,kBAAkB,QACb,aAAY;AAEnB,SAASC,mBAAmB,QAAQ,YAAW;AAK/C,SACEC,qBAAqB,EACrBC,gBAAgB,EAChBC,sBAAsB,QACjB,cAAa"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type FetchJson = (url: string) => Promise<unknown>;
|
|
2
|
+
export type JsonWebKeyLike = Record<string, unknown>;
|
|
3
|
+
export type JsonWebKeySet = {
|
|
4
|
+
keys: JsonWebKeyLike[];
|
|
5
|
+
};
|
|
6
|
+
export declare const defaultFetchJson: FetchJson;
|
|
7
|
+
export declare const getGithubOidcJwksUrl: ({ fetchJson, issuer, jwksUrl, }: {
|
|
8
|
+
fetchJson?: FetchJson;
|
|
9
|
+
issuer: string;
|
|
10
|
+
jwksUrl?: string;
|
|
11
|
+
}) => Promise<string>;
|
|
12
|
+
export declare const fetchJwks: ({ fetchJson, now, url, }: {
|
|
13
|
+
fetchJson?: FetchJson;
|
|
14
|
+
now?: Date;
|
|
15
|
+
url: string;
|
|
16
|
+
}) => Promise<JsonWebKeySet>;
|
|
17
|
+
export declare const findJwkByKid: ({ jwks, kid, }: {
|
|
18
|
+
jwks: JsonWebKeySet;
|
|
19
|
+
kid: string;
|
|
20
|
+
}) => JsonWebKeyLike | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
2
|
+
const jwksCache = new Map();
|
|
3
|
+
const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
const isJwks = (value)=>isRecord(value) && Array.isArray(value.keys) && value.keys.every((key)=>isRecord(key));
|
|
5
|
+
export const defaultFetchJson = async (url)=>{
|
|
6
|
+
const response = await fetch(url);
|
|
7
|
+
if (!response.ok) {
|
|
8
|
+
throw new Error(`Could not fetch JSON from ${url}.`);
|
|
9
|
+
}
|
|
10
|
+
return response.json();
|
|
11
|
+
};
|
|
12
|
+
export const getGithubOidcJwksUrl = async ({ fetchJson = defaultFetchJson, issuer, jwksUrl })=>{
|
|
13
|
+
if (jwksUrl) {
|
|
14
|
+
return jwksUrl;
|
|
15
|
+
}
|
|
16
|
+
const discoveryUrl = `${issuer.replace(/\/+$/g, '')}/.well-known/openid-configuration`;
|
|
17
|
+
const discovery = await fetchJson(discoveryUrl);
|
|
18
|
+
if (!isRecord(discovery) || typeof discovery.jwks_uri !== 'string') {
|
|
19
|
+
throw new Error('GitHub OIDC discovery response did not include jwks_uri.');
|
|
20
|
+
}
|
|
21
|
+
return discovery.jwks_uri;
|
|
22
|
+
};
|
|
23
|
+
export const fetchJwks = async ({ fetchJson = defaultFetchJson, now = new Date(), url })=>{
|
|
24
|
+
const cached = jwksCache.get(url);
|
|
25
|
+
if (cached && cached.expiresAt > now.getTime()) {
|
|
26
|
+
return cached.jwks;
|
|
27
|
+
}
|
|
28
|
+
const jwks = await fetchJson(url);
|
|
29
|
+
if (!isJwks(jwks)) {
|
|
30
|
+
throw new Error('GitHub OIDC JWKS response is invalid.');
|
|
31
|
+
}
|
|
32
|
+
jwksCache.set(url, {
|
|
33
|
+
expiresAt: now.getTime() + JWKS_CACHE_TTL_MS,
|
|
34
|
+
jwks
|
|
35
|
+
});
|
|
36
|
+
return jwks;
|
|
37
|
+
};
|
|
38
|
+
export const findJwkByKid = ({ jwks, kid })=>jwks.keys.find((key)=>key.kid === kid);
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=jwks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/jwks.ts"],"sourcesContent":["export type FetchJson = (url: string) => Promise<unknown>\n\nexport type JsonWebKeyLike = Record<string, unknown>\n\nexport type JsonWebKeySet = {\n keys: JsonWebKeyLike[]\n}\n\nconst JWKS_CACHE_TTL_MS = 5 * 60 * 1000\n\nconst jwksCache = new Map<\n string,\n {\n expiresAt: number\n jwks: JsonWebKeySet\n }\n>()\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst isJwks = (value: unknown): value is JsonWebKeySet =>\n isRecord(value) &&\n Array.isArray(value.keys) &&\n value.keys.every((key) => isRecord(key))\n\nexport const defaultFetchJson: FetchJson = async (url: string): Promise<unknown> => {\n const response = await fetch(url)\n\n if (!response.ok) {\n throw new Error(`Could not fetch JSON from ${url}.`)\n }\n\n return response.json() as Promise<unknown>\n}\n\nexport const getGithubOidcJwksUrl = async ({\n fetchJson = defaultFetchJson,\n issuer,\n jwksUrl,\n}: {\n fetchJson?: FetchJson\n issuer: string\n jwksUrl?: string\n}): Promise<string> => {\n if (jwksUrl) {\n return jwksUrl\n }\n\n const discoveryUrl = `${issuer.replace(/\\/+$/g, '')}/.well-known/openid-configuration`\n const discovery = await fetchJson(discoveryUrl)\n\n if (!isRecord(discovery) || typeof discovery.jwks_uri !== 'string') {\n throw new Error('GitHub OIDC discovery response did not include jwks_uri.')\n }\n\n return discovery.jwks_uri\n}\n\nexport const fetchJwks = async ({\n fetchJson = defaultFetchJson,\n now = new Date(),\n url,\n}: {\n fetchJson?: FetchJson\n now?: Date\n url: string\n}): Promise<JsonWebKeySet> => {\n const cached = jwksCache.get(url)\n\n if (cached && cached.expiresAt > now.getTime()) {\n return cached.jwks\n }\n\n const jwks = await fetchJson(url)\n\n if (!isJwks(jwks)) {\n throw new Error('GitHub OIDC JWKS response is invalid.')\n }\n\n jwksCache.set(url, {\n expiresAt: now.getTime() + JWKS_CACHE_TTL_MS,\n jwks,\n })\n\n return jwks\n}\n\nexport const findJwkByKid = ({\n jwks,\n kid,\n}: {\n jwks: JsonWebKeySet\n kid: string\n}): JsonWebKeyLike | undefined =>\n jwks.keys.find((key) => key.kid === kid)\n"],"names":["JWKS_CACHE_TTL_MS","jwksCache","Map","isRecord","value","Array","isArray","isJwks","keys","every","key","defaultFetchJson","url","response","fetch","ok","Error","json","getGithubOidcJwksUrl","fetchJson","issuer","jwksUrl","discoveryUrl","replace","discovery","jwks_uri","fetchJwks","now","Date","cached","get","expiresAt","getTime","jwks","set","findJwkByKid","kid","find"],"mappings":"AAQA,MAAMA,oBAAoB,IAAI,KAAK;AAEnC,MAAMC,YAAY,IAAIC;AAQtB,MAAMC,WAAW,CAACC,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AAEhE,MAAMG,SAAS,CAACH,QACdD,SAASC,UACTC,MAAMC,OAAO,CAACF,MAAMI,IAAI,KACxBJ,MAAMI,IAAI,CAACC,KAAK,CAAC,CAACC,MAAQP,SAASO;AAErC,OAAO,MAAMC,mBAA8B,OAAOC;IAChD,MAAMC,WAAW,MAAMC,MAAMF;IAE7B,IAAI,CAACC,SAASE,EAAE,EAAE;QAChB,MAAM,IAAIC,MAAM,CAAC,0BAA0B,EAAEJ,IAAI,CAAC,CAAC;IACrD;IAEA,OAAOC,SAASI,IAAI;AACtB,EAAC;AAED,OAAO,MAAMC,uBAAuB,OAAO,EACzCC,YAAYR,gBAAgB,EAC5BS,MAAM,EACNC,OAAO,EAKR;IACC,IAAIA,SAAS;QACX,OAAOA;IACT;IAEA,MAAMC,eAAe,GAAGF,OAAOG,OAAO,CAAC,SAAS,IAAI,iCAAiC,CAAC;IACtF,MAAMC,YAAY,MAAML,UAAUG;IAElC,IAAI,CAACnB,SAASqB,cAAc,OAAOA,UAAUC,QAAQ,KAAK,UAAU;QAClE,MAAM,IAAIT,MAAM;IAClB;IAEA,OAAOQ,UAAUC,QAAQ;AAC3B,EAAC;AAED,OAAO,MAAMC,YAAY,OAAO,EAC9BP,YAAYR,gBAAgB,EAC5BgB,MAAM,IAAIC,MAAM,EAChBhB,GAAG,EAKJ;IACC,MAAMiB,SAAS5B,UAAU6B,GAAG,CAAClB;IAE7B,IAAIiB,UAAUA,OAAOE,SAAS,GAAGJ,IAAIK,OAAO,IAAI;QAC9C,OAAOH,OAAOI,IAAI;IACpB;IAEA,MAAMA,OAAO,MAAMd,UAAUP;IAE7B,IAAI,CAACL,OAAO0B,OAAO;QACjB,MAAM,IAAIjB,MAAM;IAClB;IAEAf,UAAUiC,GAAG,CAACtB,KAAK;QACjBmB,WAAWJ,IAAIK,OAAO,KAAKhC;QAC3BiC;IACF;IAEA,OAAOA;AACT,EAAC;AAED,OAAO,MAAME,eAAe,CAAC,EAC3BF,IAAI,EACJG,GAAG,EAIJ,GACCH,KAAKzB,IAAI,CAAC6B,IAAI,CAAC,CAAC3B,MAAQA,IAAI0B,GAAG,KAAKA,KAAI"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type DecodedJwt = {
|
|
2
|
+
encodedHeader: string;
|
|
3
|
+
encodedPayload: string;
|
|
4
|
+
header: Record<string, unknown>;
|
|
5
|
+
payload: Record<string, unknown>;
|
|
6
|
+
signature: Buffer;
|
|
7
|
+
signingInput: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const decodeJwt: (token: string) => DecodedJwt | undefined;
|
|
10
|
+
export declare const toBase64Url: (input: Buffer | string) => string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const base64UrlToBuffer = (input)=>{
|
|
2
|
+
const normalized = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
3
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, '=');
|
|
4
|
+
return Buffer.from(padded, 'base64');
|
|
5
|
+
};
|
|
6
|
+
const parseJsonObject = (buffer)=>{
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(buffer.toString('utf8'));
|
|
9
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
};
|
|
17
|
+
export const decodeJwt = (token)=>{
|
|
18
|
+
const parts = token.split('.');
|
|
19
|
+
if (parts.length !== 3) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
23
|
+
if (!encodedHeader || !encodedPayload || !encodedSignature) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const header = parseJsonObject(base64UrlToBuffer(encodedHeader));
|
|
27
|
+
const payload = parseJsonObject(base64UrlToBuffer(encodedPayload));
|
|
28
|
+
if (!header || !payload) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
encodedHeader,
|
|
33
|
+
encodedPayload,
|
|
34
|
+
header,
|
|
35
|
+
payload,
|
|
36
|
+
signature: base64UrlToBuffer(encodedSignature),
|
|
37
|
+
signingInput: `${encodedHeader}.${encodedPayload}`
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export const toBase64Url = (input)=>Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=jwt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/jwt.ts"],"sourcesContent":["export type DecodedJwt = {\n encodedHeader: string\n encodedPayload: string\n header: Record<string, unknown>\n payload: Record<string, unknown>\n signature: Buffer\n signingInput: string\n}\n\nconst base64UrlToBuffer = (input: string): Buffer => {\n const normalized = input.replace(/-/g, '+').replace(/_/g, '/')\n const padded = normalized.padEnd(\n normalized.length + ((4 - (normalized.length % 4)) % 4),\n '=',\n )\n\n return Buffer.from(padded, 'base64')\n}\n\nconst parseJsonObject = (buffer: Buffer): Record<string, unknown> | undefined => {\n try {\n const parsed = JSON.parse(buffer.toString('utf8')) as unknown\n\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n } catch {\n return undefined\n }\n\n return undefined\n}\n\nexport const decodeJwt = (token: string): DecodedJwt | undefined => {\n const parts = token.split('.')\n\n if (parts.length !== 3) {\n return undefined\n }\n\n const [encodedHeader, encodedPayload, encodedSignature] = parts\n\n if (!encodedHeader || !encodedPayload || !encodedSignature) {\n return undefined\n }\n\n const header = parseJsonObject(base64UrlToBuffer(encodedHeader))\n const payload = parseJsonObject(base64UrlToBuffer(encodedPayload))\n\n if (!header || !payload) {\n return undefined\n }\n\n return {\n encodedHeader,\n encodedPayload,\n header,\n payload,\n signature: base64UrlToBuffer(encodedSignature),\n signingInput: `${encodedHeader}.${encodedPayload}`,\n }\n}\n\nexport const toBase64Url = (input: Buffer | string): string =>\n Buffer.from(input)\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '')\n"],"names":["base64UrlToBuffer","input","normalized","replace","padded","padEnd","length","Buffer","from","parseJsonObject","buffer","parsed","JSON","parse","toString","Array","isArray","undefined","decodeJwt","token","parts","split","encodedHeader","encodedPayload","encodedSignature","header","payload","signature","signingInput","toBase64Url"],"mappings":"AASA,MAAMA,oBAAoB,CAACC;IACzB,MAAMC,aAAaD,MAAME,OAAO,CAAC,MAAM,KAAKA,OAAO,CAAC,MAAM;IAC1D,MAAMC,SAASF,WAAWG,MAAM,CAC9BH,WAAWI,MAAM,GAAI,AAAC,CAAA,IAAKJ,WAAWI,MAAM,GAAG,CAAC,IAAK,GACrD;IAGF,OAAOC,OAAOC,IAAI,CAACJ,QAAQ;AAC7B;AAEA,MAAMK,kBAAkB,CAACC;IACvB,IAAI;QACF,MAAMC,SAASC,KAAKC,KAAK,CAACH,OAAOI,QAAQ,CAAC;QAE1C,IAAI,OAAOH,WAAW,YAAYA,WAAW,QAAQ,CAACI,MAAMC,OAAO,CAACL,SAAS;YAC3E,OAAOA;QACT;IACF,EAAE,OAAM;QACN,OAAOM;IACT;IAEA,OAAOA;AACT;AAEA,OAAO,MAAMC,YAAY,CAACC;IACxB,MAAMC,QAAQD,MAAME,KAAK,CAAC;IAE1B,IAAID,MAAMd,MAAM,KAAK,GAAG;QACtB,OAAOW;IACT;IAEA,MAAM,CAACK,eAAeC,gBAAgBC,iBAAiB,GAAGJ;IAE1D,IAAI,CAACE,iBAAiB,CAACC,kBAAkB,CAACC,kBAAkB;QAC1D,OAAOP;IACT;IAEA,MAAMQ,SAAShB,gBAAgBT,kBAAkBsB;IACjD,MAAMI,UAAUjB,gBAAgBT,kBAAkBuB;IAElD,IAAI,CAACE,UAAU,CAACC,SAAS;QACvB,OAAOT;IACT;IAEA,OAAO;QACLK;QACAC;QACAE;QACAC;QACAC,WAAW3B,kBAAkBwB;QAC7BI,cAAc,GAAGN,cAAc,CAAC,EAAEC,gBAAgB;IACpD;AACF,EAAC;AAED,OAAO,MAAMM,cAAc,CAAC5B,QAC1BM,OAAOC,IAAI,CAACP,OACTa,QAAQ,CAAC,UACTX,OAAO,CAAC,OAAO,KACfA,OAAO,CAAC,OAAO,KACfA,OAAO,CAAC,QAAQ,IAAG"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type NoncePayloadOperations = {
|
|
2
|
+
create: (args: {
|
|
3
|
+
collection: string;
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
overrideAccess?: boolean;
|
|
6
|
+
}) => Promise<Record<string, unknown>>;
|
|
7
|
+
find: (args: {
|
|
8
|
+
collection: string;
|
|
9
|
+
depth?: number;
|
|
10
|
+
limit?: number;
|
|
11
|
+
overrideAccess?: boolean;
|
|
12
|
+
where?: unknown;
|
|
13
|
+
}) => Promise<{
|
|
14
|
+
docs: unknown[];
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
export declare const assertNonceNotReplayed: ({ collectionSlug, keyId, nonce, now, payload, }: {
|
|
18
|
+
collectionSlug: string;
|
|
19
|
+
keyId: string;
|
|
20
|
+
nonce: string;
|
|
21
|
+
now: Date;
|
|
22
|
+
payload: NoncePayloadOperations;
|
|
23
|
+
}) => Promise<boolean>;
|
|
24
|
+
export declare const storeAcceptedNonce: ({ bodyHash, collectionSlug, expiresAt, keyId, nonce, payload, sourceId, syncRunId, usedAt, }: {
|
|
25
|
+
bodyHash: string;
|
|
26
|
+
collectionSlug: string;
|
|
27
|
+
expiresAt: Date;
|
|
28
|
+
keyId: string;
|
|
29
|
+
nonce: string;
|
|
30
|
+
payload: NoncePayloadOperations;
|
|
31
|
+
sourceId: string;
|
|
32
|
+
syncRunId?: string;
|
|
33
|
+
usedAt: Date;
|
|
34
|
+
}) => Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const assertNonceNotReplayed = async ({ collectionSlug, keyId, nonce, now, payload })=>{
|
|
2
|
+
const result = await payload.find({
|
|
3
|
+
collection: collectionSlug,
|
|
4
|
+
depth: 0,
|
|
5
|
+
limit: 1,
|
|
6
|
+
overrideAccess: true,
|
|
7
|
+
where: {
|
|
8
|
+
and: [
|
|
9
|
+
{
|
|
10
|
+
keyId: {
|
|
11
|
+
equals: keyId
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
nonce: {
|
|
16
|
+
equals: nonce
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
expiresAt: {
|
|
21
|
+
greater_than: now.toISOString()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return result.docs.length === 0;
|
|
28
|
+
};
|
|
29
|
+
export const storeAcceptedNonce = async ({ bodyHash, collectionSlug, expiresAt, keyId, nonce, payload, sourceId, syncRunId, usedAt })=>payload.create({
|
|
30
|
+
collection: collectionSlug,
|
|
31
|
+
data: {
|
|
32
|
+
bodyHash,
|
|
33
|
+
expiresAt: expiresAt.toISOString(),
|
|
34
|
+
keyId,
|
|
35
|
+
nonce,
|
|
36
|
+
sourceId,
|
|
37
|
+
syncRunId,
|
|
38
|
+
usedAt: usedAt.toISOString()
|
|
39
|
+
},
|
|
40
|
+
overrideAccess: true
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=nonce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/nonce.ts"],"sourcesContent":["export type NoncePayloadOperations = {\n create: (args: {\n collection: string\n data: Record<string, unknown>\n overrideAccess?: boolean\n }) => Promise<Record<string, unknown>>\n find: (args: {\n collection: string\n depth?: number\n limit?: number\n overrideAccess?: boolean\n where?: unknown\n }) => Promise<{\n docs: unknown[]\n }>\n}\n\nexport const assertNonceNotReplayed = async ({\n collectionSlug,\n keyId,\n nonce,\n now,\n payload,\n}: {\n collectionSlug: string\n keyId: string\n nonce: string\n now: Date\n payload: NoncePayloadOperations\n}): Promise<boolean> => {\n const result = await payload.find({\n collection: collectionSlug,\n depth: 0,\n limit: 1,\n overrideAccess: true,\n where: {\n and: [\n {\n keyId: {\n equals: keyId,\n },\n },\n {\n nonce: {\n equals: nonce,\n },\n },\n {\n expiresAt: {\n greater_than: now.toISOString(),\n },\n },\n ],\n },\n })\n\n return result.docs.length === 0\n}\n\nexport const storeAcceptedNonce = async ({\n bodyHash,\n collectionSlug,\n expiresAt,\n keyId,\n nonce,\n payload,\n sourceId,\n syncRunId,\n usedAt,\n}: {\n bodyHash: string\n collectionSlug: string\n expiresAt: Date\n keyId: string\n nonce: string\n payload: NoncePayloadOperations\n sourceId: string\n syncRunId?: string\n usedAt: Date\n}): Promise<Record<string, unknown>> =>\n payload.create({\n collection: collectionSlug,\n data: {\n bodyHash,\n expiresAt: expiresAt.toISOString(),\n keyId,\n nonce,\n sourceId,\n syncRunId,\n usedAt: usedAt.toISOString(),\n },\n overrideAccess: true,\n })\n\n"],"names":["assertNonceNotReplayed","collectionSlug","keyId","nonce","now","payload","result","find","collection","depth","limit","overrideAccess","where","and","equals","expiresAt","greater_than","toISOString","docs","length","storeAcceptedNonce","bodyHash","sourceId","syncRunId","usedAt","create","data"],"mappings":"AAiBA,OAAO,MAAMA,yBAAyB,OAAO,EAC3CC,cAAc,EACdC,KAAK,EACLC,KAAK,EACLC,GAAG,EACHC,OAAO,EAOR;IACC,MAAMC,SAAS,MAAMD,QAAQE,IAAI,CAAC;QAChCC,YAAYP;QACZQ,OAAO;QACPC,OAAO;QACPC,gBAAgB;QAChBC,OAAO;YACLC,KAAK;gBACH;oBACEX,OAAO;wBACLY,QAAQZ;oBACV;gBACF;gBACA;oBACEC,OAAO;wBACLW,QAAQX;oBACV;gBACF;gBACA;oBACEY,WAAW;wBACTC,cAAcZ,IAAIa,WAAW;oBAC/B;gBACF;aACD;QACH;IACF;IAEA,OAAOX,OAAOY,IAAI,CAACC,MAAM,KAAK;AAChC,EAAC;AAED,OAAO,MAAMC,qBAAqB,OAAO,EACvCC,QAAQ,EACRpB,cAAc,EACdc,SAAS,EACTb,KAAK,EACLC,KAAK,EACLE,OAAO,EACPiB,QAAQ,EACRC,SAAS,EACTC,MAAM,EAWP,GACCnB,QAAQoB,MAAM,CAAC;QACbjB,YAAYP;QACZyB,MAAM;YACJL;YACAN,WAAWA,UAAUE,WAAW;YAChCf;YACAC;YACAmB;YACAC;YACAC,QAAQA,OAAOP,WAAW;QAC5B;QACAN,gBAAgB;IAClB,GAAE"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type SignDocsSyncRequestOptions = {
|
|
2
|
+
body: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
keyId: string;
|
|
5
|
+
nonce?: string;
|
|
6
|
+
now?: Date;
|
|
7
|
+
privateKey: string;
|
|
8
|
+
};
|
|
9
|
+
export type SignedDocsSyncRequest = {
|
|
10
|
+
body: string;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
export declare const signDocsSyncRequest: ({ body, endpoint, keyId, nonce, now, privateKey, }: SignDocsSyncRequestOptions) => SignedDocsSyncRequest;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createPrivateKey, randomUUID, sign } from 'node:crypto';
|
|
2
|
+
import { sha256Hex } from '../sync/index.js';
|
|
3
|
+
import { buildCanonicalSigningString } from './canonical.js';
|
|
4
|
+
const getPrivateKeyInput = (privateKey)=>{
|
|
5
|
+
if (privateKey.includes('BEGIN PRIVATE KEY')) {
|
|
6
|
+
return privateKey;
|
|
7
|
+
}
|
|
8
|
+
return createPrivateKey({
|
|
9
|
+
type: 'pkcs8',
|
|
10
|
+
format: 'der',
|
|
11
|
+
key: Buffer.from(privateKey, 'base64')
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
const getEndpointPathname = (endpoint)=>new URL(endpoint).pathname;
|
|
15
|
+
export const signDocsSyncRequest = ({ body, endpoint, keyId, nonce = randomUUID(), now = new Date(), privateKey })=>{
|
|
16
|
+
const bodySha256 = sha256Hex(body);
|
|
17
|
+
const timestamp = now.toISOString();
|
|
18
|
+
const canonicalString = buildCanonicalSigningString({
|
|
19
|
+
bodySha256,
|
|
20
|
+
method: 'POST',
|
|
21
|
+
nonce,
|
|
22
|
+
path: getEndpointPathname(endpoint),
|
|
23
|
+
timestamp
|
|
24
|
+
});
|
|
25
|
+
const signature = sign(null, Buffer.from(canonicalString, 'utf8'), getPrivateKeyInput(privateKey)).toString('base64');
|
|
26
|
+
return {
|
|
27
|
+
body,
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'X-VL-MD-DOCS-Body-SHA256': bodySha256,
|
|
31
|
+
'X-VL-MD-DOCS-Key-Id': keyId,
|
|
32
|
+
'X-VL-MD-DOCS-Nonce': nonce,
|
|
33
|
+
'X-VL-MD-DOCS-Signature': signature,
|
|
34
|
+
'X-VL-MD-DOCS-Timestamp': timestamp
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=sign.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/security/sign.ts"],"sourcesContent":["import {\n createPrivateKey,\n randomUUID,\n sign,\n} from 'node:crypto'\n\nimport { sha256Hex } from '../sync/index.js'\nimport { buildCanonicalSigningString } from './canonical.js'\n\nexport type SignDocsSyncRequestOptions = {\n body: string\n endpoint: string\n keyId: string\n nonce?: string\n now?: Date\n privateKey: string\n}\n\nexport type SignedDocsSyncRequest = {\n body: string\n headers: Record<string, string>\n}\n\nconst getPrivateKeyInput = (privateKey: string) => {\n if (privateKey.includes('BEGIN PRIVATE KEY')) {\n return privateKey\n }\n\n return createPrivateKey({\n type: 'pkcs8',\n format: 'der',\n key: Buffer.from(privateKey, 'base64'),\n })\n}\n\nconst getEndpointPathname = (endpoint: string): string => new URL(endpoint).pathname\n\nexport const signDocsSyncRequest = ({\n body,\n endpoint,\n keyId,\n nonce = randomUUID(),\n now = new Date(),\n privateKey,\n}: SignDocsSyncRequestOptions): SignedDocsSyncRequest => {\n const bodySha256 = sha256Hex(body)\n const timestamp = now.toISOString()\n const canonicalString = buildCanonicalSigningString({\n bodySha256,\n method: 'POST',\n nonce,\n path: getEndpointPathname(endpoint),\n timestamp,\n })\n const signature = sign(\n null,\n Buffer.from(canonicalString, 'utf8'),\n getPrivateKeyInput(privateKey),\n ).toString('base64')\n\n return {\n body,\n headers: {\n 'Content-Type': 'application/json',\n 'X-VL-MD-DOCS-Body-SHA256': bodySha256,\n 'X-VL-MD-DOCS-Key-Id': keyId,\n 'X-VL-MD-DOCS-Nonce': nonce,\n 'X-VL-MD-DOCS-Signature': signature,\n 'X-VL-MD-DOCS-Timestamp': timestamp,\n },\n }\n}\n"],"names":["createPrivateKey","randomUUID","sign","sha256Hex","buildCanonicalSigningString","getPrivateKeyInput","privateKey","includes","type","format","key","Buffer","from","getEndpointPathname","endpoint","URL","pathname","signDocsSyncRequest","body","keyId","nonce","now","Date","bodySha256","timestamp","toISOString","canonicalString","method","path","signature","toString","headers"],"mappings":"AAAA,SACEA,gBAAgB,EAChBC,UAAU,EACVC,IAAI,QACC,cAAa;AAEpB,SAASC,SAAS,QAAQ,mBAAkB;AAC5C,SAASC,2BAA2B,QAAQ,iBAAgB;AAgB5D,MAAMC,qBAAqB,CAACC;IAC1B,IAAIA,WAAWC,QAAQ,CAAC,sBAAsB;QAC5C,OAAOD;IACT;IAEA,OAAON,iBAAiB;QACtBQ,MAAM;QACNC,QAAQ;QACRC,KAAKC,OAAOC,IAAI,CAACN,YAAY;IAC/B;AACF;AAEA,MAAMO,sBAAsB,CAACC,WAA6B,IAAIC,IAAID,UAAUE,QAAQ;AAEpF,OAAO,MAAMC,sBAAsB,CAAC,EAClCC,IAAI,EACJJ,QAAQ,EACRK,KAAK,EACLC,QAAQnB,YAAY,EACpBoB,MAAM,IAAIC,MAAM,EAChBhB,UAAU,EACiB;IAC3B,MAAMiB,aAAapB,UAAUe;IAC7B,MAAMM,YAAYH,IAAII,WAAW;IACjC,MAAMC,kBAAkBtB,4BAA4B;QAClDmB;QACAI,QAAQ;QACRP;QACAQ,MAAMf,oBAAoBC;QAC1BU;IACF;IACA,MAAMK,YAAY3B,KAChB,MACAS,OAAOC,IAAI,CAACc,iBAAiB,SAC7BrB,mBAAmBC,aACnBwB,QAAQ,CAAC;IAEX,OAAO;QACLZ;QACAa,SAAS;YACP,gBAAgB;YAChB,4BAA4BR;YAC5B,uBAAuBJ;YACvB,sBAAsBC;YACtB,0BAA0BS;YAC1B,0BAA0BL;QAC5B;IACF;AACF,EAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type VerifyBodyHashResult = {
|
|
2
|
+
computedHash: string;
|
|
3
|
+
ok: false;
|
|
4
|
+
} | {
|
|
5
|
+
computedHash: string;
|
|
6
|
+
ok: true;
|
|
7
|
+
};
|
|
8
|
+
export type ValidateTimestampResult = {
|
|
9
|
+
date: Date;
|
|
10
|
+
ok: true;
|
|
11
|
+
} | {
|
|
12
|
+
message: string;
|
|
13
|
+
ok: false;
|
|
14
|
+
};
|
|
15
|
+
export declare const verifyBodySha256: ({ body, expectedHash, }: {
|
|
16
|
+
body: string;
|
|
17
|
+
expectedHash: string;
|
|
18
|
+
}) => VerifyBodyHashResult;
|
|
19
|
+
export declare const validateTimestampSkew: ({ maxSkewSeconds, now, timestamp, }: {
|
|
20
|
+
maxSkewSeconds: number;
|
|
21
|
+
now?: Date;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
}) => ValidateTimestampResult;
|
|
24
|
+
export declare const verifyEd25519Signature: ({ canonicalString, publicKey, signature, }: {
|
|
25
|
+
canonicalString: string;
|
|
26
|
+
publicKey: string;
|
|
27
|
+
signature: string;
|
|
28
|
+
}) => boolean;
|