@pulse-compute/jwt 1.0.0-beta.4 → 1.0.0-beta.6

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.
@@ -483,7 +483,101 @@ function parseEs256Jwk(ts, sourceFile, node, diagnostics) {
483
483
  return deepFreeze(output);
484
484
  }
485
485
 
486
- function parseEs256Jwks(ts, sourceFile, node, diagnostics) {
486
+ function parseRs256Jwk(ts, sourceFile, node, diagnostics) {
487
+ const members = objectMembers(
488
+ ts,
489
+ sourceFile,
490
+ node,
491
+ new Set(['kty', 'n', 'e', 'alg', 'use', 'key_ops', 'kid']),
492
+ diagnostics,
493
+ jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID,
494
+ 'RS256 public JWK'
495
+ );
496
+ const required = {};
497
+ for (const name of ['kty', 'n', 'e']) {
498
+ const value = members.has(name) ? literalString(ts, members.get(name)) : undefined;
499
+ if (value === undefined || value.length === 0) {
500
+ diagnostics.push(makeDiagnostic(
501
+ sourceFile,
502
+ members.get(name) || node,
503
+ jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID,
504
+ `RS256 public JWK requires static ${name}.`,
505
+ 'Use a bounded inline public RSA JWK.'
506
+ ));
507
+ } else required[name] = value;
508
+ }
509
+ if (required.kty !== undefined && required.kty !== 'RSA') diagnostics.push(makeDiagnostic(
510
+ sourceFile,
511
+ members.get('kty') || node,
512
+ jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID,
513
+ 'RS256 public JWK kty must be RSA.',
514
+ 'Use kty: "RSA".'
515
+ ));
516
+ for (const name of ['n', 'e']) {
517
+ const text = required[name];
518
+ const bytes = typeof text === 'string' && /^[A-Za-z0-9_-]+$/.test(text) && text.length <= 683 ? Buffer.from(text, 'base64url') : Buffer.alloc(0);
519
+ const valid = bytes.length > 0 && bytes[0] !== 0 && bytes.toString('base64url') === text
520
+ && (name === 'n' ? [256, 384, 512].includes(bytes.length) && bytes[0] >= 128 && (bytes.at(-1) & 1)
521
+ : bytes.length <= 4 && bytes.readUIntBE(0, bytes.length) >= 3 && (bytes.at(-1) & 1));
522
+ if (!valid) diagnostics.push(makeDiagnostic(sourceFile, members.get(name) || node,
523
+ jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID, 'RSA requires canonical 2048/3072/4096-bit n and an odd 32-bit exponent >= 3.', 'Use a supported public RSA JWK.'));
524
+ }
525
+
526
+ const output = { ...required };
527
+ for (const name of ['alg', 'use', 'kid']) {
528
+ if (!members.has(name)) continue;
529
+ const value = parseOptionalString(
530
+ ts,
531
+ sourceFile,
532
+ members.get(name),
533
+ diagnostics,
534
+ `RS256 public JWK ${name}`,
535
+ jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID
536
+ );
537
+ if (value !== undefined) output[name] = value;
538
+ }
539
+ if (output.alg !== undefined && output.alg !== 'RS256') diagnostics.push(makeDiagnostic(
540
+ sourceFile,
541
+ members.get('alg'),
542
+ jwtContracts.JWT_DIAGNOSTIC_CODES.KEY_ALGORITHM_MISMATCH,
543
+ 'RS256 public JWK alg must be absent or RS256.',
544
+ 'Remove alg or use alg: "RS256".'
545
+ ));
546
+ if (output.use !== undefined && output.use !== 'sig') diagnostics.push(makeDiagnostic(
547
+ sourceFile,
548
+ members.get('use'),
549
+ jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID,
550
+ 'RS256 public JWK use must be absent or sig.',
551
+ 'Remove use or use use: "sig".'
552
+ ));
553
+ if (members.has('key_ops')) {
554
+ const operations = literalStringArray(
555
+ ts,
556
+ sourceFile,
557
+ members.get('key_ops'),
558
+ diagnostics,
559
+ {
560
+ label: 'RS256 public JWK key_ops',
561
+ category: 'jwk-key-ops',
562
+ maximum: 1,
563
+ code: jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID,
564
+ limitCode: jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID
565
+ }
566
+ );
567
+ if (operations.length !== 1 || operations[0] !== 'verify') diagnostics.push(makeDiagnostic(
568
+ sourceFile,
569
+ members.get('key_ops'),
570
+ jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID,
571
+ 'RS256 public JWK key_ops must be absent or exactly ["verify"].',
572
+ 'Remove key_ops or declare only verify.'
573
+ ));
574
+ output.key_ops = operations;
575
+ }
576
+ if (output.kid !== undefined && Buffer.byteLength(output.kid) > 256) diagnostics.push(makeDiagnostic(sourceFile, members.get('kid'), jwtContracts.JWT_DIAGNOSTIC_CODES.JWK_INVALID, 'RSA kid exceeds 256 UTF-8 bytes.', 'Use a bounded kid.'));
577
+ return deepFreeze(output);
578
+ }
579
+
580
+ function parseEs256Jwks(ts, sourceFile, node, diagnostics, algorithm = 'ES256') {
487
581
  const current = unwrapExpression(ts, node);
488
582
  if (!current || !ts.isArrayLiteralExpression(current)) {
489
583
  diagnostics.push(makeDiagnostic(
@@ -514,7 +608,7 @@ function parseEs256Jwks(ts, sourceFile, node, diagnostics) {
514
608
  }
515
609
  ));
516
610
  const keys = current.elements.slice(0, jwtContracts.JWT_RESOURCE_LIMITS.jwksEntries)
517
- .map((entry) => parseEs256Jwk(ts, sourceFile, entry, diagnostics));
611
+ .map((entry) => (algorithm === 'RS256' ? parseRs256Jwk : parseEs256Jwk)(ts, sourceFile, entry, diagnostics));
518
612
  const kids = keys.map((entry) => entry.kid).filter(Boolean);
519
613
  if (new Set(kids).size !== kids.length) diagnostics.push(makeDiagnostic(
520
614
  sourceFile,
@@ -527,13 +621,14 @@ function parseEs256Jwks(ts, sourceFile, node, diagnostics) {
527
621
  }
528
622
 
529
623
  function keyArtifact(type, keys) {
624
+ const algorithm = keys[0]?.kty === 'RSA' ? 'rs256' : 'es256';
530
625
  const material = type === 'jwk'
531
626
  ? { type, key: keys[0] }
532
627
  : { type, keys };
533
628
  const materialHash = crypto.createHash('sha256')
534
629
  .update(stableStringify(material))
535
630
  .digest('hex');
536
- const id = `pulse.jwt.es256-key.${materialHash.slice(0, 24)}`;
631
+ const id = `pulse.jwt.${algorithm}-key.${materialHash.slice(0, 24)}`;
537
632
  return Object.freeze({
538
633
  descriptor: Object.freeze({
539
634
  type,
@@ -547,14 +642,14 @@ function keyArtifact(type, keys) {
547
642
  secretBinding: null
548
643
  }),
549
644
  realizationArtifact: deepFreeze({
550
- version: 'pulse.jwt-es256-key-artifact.v1',
645
+ version: `pulse.jwt-${algorithm}-key-artifact.v1`,
551
646
  id,
552
647
  contractId: jwtContracts.JWT_CONTRACT_ID,
553
648
  package: jwtContracts.JWT_PACKAGE_NAME,
554
- kind: 'jwt-es256-static-public-key',
555
- mediaType: 'application/vnd.pulse.jwt-es256-key+json',
649
+ kind: `jwt-${algorithm}-static-public-key`,
650
+ mediaType: `application/vnd.pulse.jwt-${algorithm}-key+json`,
556
651
  materialHash,
557
- redaction: ['x', 'y'],
652
+ redaction: algorithm === 'rs256' ? ['n', 'e'] : ['x', 'y'],
558
653
  data: material
559
654
  })
560
655
  });
@@ -620,7 +715,7 @@ function parseKey(ts, sourceFile, node, algorithms, diagnostics) {
620
715
  });
621
716
  }
622
717
 
623
- if (algorithms.length !== 1 || algorithms[0] !== 'ES256') {
718
+ if (algorithms.length !== 1 || !['ES256', 'RS256'].includes(algorithms[0])) {
624
719
  diagnostics.push(makeDiagnostic(
625
720
  sourceFile,
626
721
  node,
@@ -649,7 +744,7 @@ function parseKey(ts, sourceFile, node, algorithms, diagnostics) {
649
744
  ));
650
745
  return keyArtifact(
651
746
  type,
652
- [parseEs256Jwk(ts, sourceFile, members.get('key'), diagnostics)]
747
+ [(algorithms[0] === 'RS256' ? parseRs256Jwk : parseEs256Jwk)(ts, sourceFile, members.get('key'), diagnostics)]
653
748
  );
654
749
  }
655
750
  if (members.size !== 2 || !members.has('keys')) diagnostics.push(makeDiagnostic(
@@ -661,7 +756,7 @@ function parseKey(ts, sourceFile, node, algorithms, diagnostics) {
661
756
  ));
662
757
  return keyArtifact(
663
758
  type,
664
- parseEs256Jwks(ts, sourceFile, members.get('keys'), diagnostics)
759
+ parseEs256Jwks(ts, sourceFile, members.get('keys'), diagnostics, algorithms[0])
665
760
  );
666
761
  }
667
762
 
@@ -797,6 +892,7 @@ function collectFacadeBindings(ts, sourceFile, manifest) {
797
892
  const bindings = {
798
893
  namespaces: new Set(),
799
894
  verify: new Set(),
895
+ sign: new Set(),
800
896
  bearer: new Set(),
801
897
  imports: []
802
898
  };
@@ -821,6 +917,7 @@ function collectFacadeBindings(ts, sourceFile, manifest) {
821
917
  const local = specifier.name.text;
822
918
  if (imported === 'jwt' || imported === 'default') bindings.namespaces.add(local);
823
919
  if (imported === 'verify') bindings.verify.add(local);
920
+ if (imported === 'sign') bindings.sign.add(local);
824
921
  if (imported === 'bearer') bindings.bearer.add(local);
825
922
  bindings.imports.push({ imported, local, loc: locFor(sourceFile, specifier.name) });
826
923
  }
@@ -834,13 +931,14 @@ function resolveFacadeCall(ts, call, bindings) {
834
931
  if (expression && ts.isPropertyAccessExpression(expression)) {
835
932
  const object = unwrapExpression(ts, expression.expression);
836
933
  if (object && ts.isIdentifier(object) && bindings.namespaces.has(object.text)) {
837
- if (expression.name.text === 'verify' || expression.name.text === 'bearer') {
934
+ if (['verify', 'bearer', 'sign'].includes(expression.name.text)) {
838
935
  return { method: expression.name.text, local: object.text };
839
936
  }
840
937
  }
841
938
  }
842
939
  if (expression && ts.isIdentifier(expression)) {
843
940
  if (bindings.verify.has(expression.text)) return { method: 'verify', local: expression.text };
941
+ if (bindings.sign.has(expression.text)) return { method: 'sign', local: expression.text };
844
942
  if (bindings.bearer.has(expression.text)) return { method: 'bearer', local: expression.text };
845
943
  }
846
944
  return undefined;
@@ -1066,6 +1164,45 @@ function buildJwtLoweringPlan(inputs = {}) {
1066
1164
  function visit(node) {
1067
1165
  if (ts.isCallExpression(node)) {
1068
1166
  const resolved = resolveFacadeCall(ts, node, bindings);
1167
+ if (resolved && resolved.method === 'sign') {
1168
+ const contextName = enclosingContextName(ts, node);
1169
+ const placement = placementForCall(ts, node, contextName);
1170
+ const error = (code, message) => diagnostics.push(makeDiagnostic(sourceFile, node, code, message,
1171
+ 'Await jwt.sign(ctx, claims, { algorithm: "HS256", key: { type: "secret", binding: "NAME" }, expiresInSeconds: 45 }) into a local or in ctx.parallel.'));
1172
+ if (node.arguments.length !== 3 || !currentContextArgument(ts, node.arguments[0], contextName)) {
1173
+ error(jwtContracts.JWT_DIAGNOSTIC_CODES.ARGUMENT_SHAPE_UNSUPPORTED, 'Signing requires the current handler context, claims, and static options.');
1174
+ return;
1175
+ }
1176
+ if (!(placement.kind === 'variable' && placement.awaited || placement.parallel && ts.isAwaitExpression(placement.parallel.parent))) {
1177
+ error(jwtContracts.JWT_DIAGNOSTIC_CODES.PLACEMENT_UNSUPPORTED, 'Signing must be directly awaited or grouped.');
1178
+ }
1179
+ const members = objectMembers(ts, sourceFile, node.arguments[2], new Set(['algorithm', 'key', 'expiresInSeconds', 'kid']), diagnostics,
1180
+ jwtContracts.JWT_DIAGNOSTIC_CODES.OPTIONS_LITERAL_REQUIRED, 'JWT signing options');
1181
+ const algorithm = literalString(ts, members.get('algorithm'));
1182
+ const expiresInSeconds = literalNumber(ts, members.get('expiresInSeconds'));
1183
+ if (!['HS256', 'ES256', 'RS256'].includes(algorithm)) error(jwtContracts.JWT_DIAGNOSTIC_CODES.ALGORITHM_UNSUPPORTED, 'Signing supports HS256, ES256 and RS256.');
1184
+ const kid = members.has('kid') ? literalString(ts, members.get('kid')) : undefined;
1185
+ if (members.has('kid') && (typeof kid !== 'string' || !kid.length || kid.includes('\0') || Buffer.byteLength(kid) > 256)) {
1186
+ error(jwtContracts.JWT_DIAGNOSTIC_CODES.KEY_TYPE_UNSUPPORTED, 'Signing kid must be a nonempty literal of at most 256 UTF-8 bytes.');
1187
+ }
1188
+ if (!Number.isSafeInteger(expiresInSeconds) || expiresInSeconds < 1) {
1189
+ error(jwtContracts.JWT_DIAGNOSTIC_CODES.POLICY_LIMIT_EXCEEDED, 'Signing requires a literal positive safe-integer lifetime in seconds.');
1190
+ }
1191
+ const key = parseKey(ts, sourceFile, members.get('key') || node, ['HS256'], diagnostics);
1192
+ if (key.descriptor.type !== 'secret' || !key.resource.secretBinding || key.resource.secretBinding.length > 256
1193
+ || key.resource.secretBinding.includes('\0') || !key.resource.secretBinding.trim()) {
1194
+ error(jwtContracts.JWT_DIAGNOSTIC_CODES.KEY_TYPE_UNSUPPORTED, 'Signing requires a bounded named secret binding.');
1195
+ }
1196
+ const effect = Object.freeze({ ...jwtContracts.JWT_SIGN_OPERATION, placement: placement.kind,
1197
+ range: rangeFor(sourceFile, node), loc: locFor(sourceFile, node), resource: key.resource,
1198
+ payload: { algorithm, expiresInSeconds: expiresInSeconds || 0, ...(kid === undefined ? {} : { kid }) },
1199
+ runtimeInputs: [{ name: 'claims', argumentIndex: 1, source: 'package-call-argument' }],
1200
+ providerRequirements: jwtContracts.JWT_SIGN_CONTRACT.providerRequirements,
1201
+ schemaReferences: [], redaction: ['claims', 'token', 'signature', 'key-material', 'secret-value'] });
1202
+ entries.push(Object.freeze({ kind: 'jwt-sign', symbol: 'jwt.sign', placement: placement.kind,
1203
+ awaited: placement.awaited, algorithms: [], signingAlgorithm: algorithm, key: key.descriptor,
1204
+ providerRequirements: effect.providerRequirements, range: effect.range, loc: effect.loc, canonicalEffect: effect }));
1205
+ }
1069
1206
  if (resolved && resolved.method === 'bearer') bearerCalls.push(node);
1070
1207
  if (resolved && resolved.method === 'verify') {
1071
1208
  const contextName = enclosingContextName(ts, node);
@@ -1141,14 +1278,18 @@ function buildJwtLoweringPlan(inputs = {}) {
1141
1278
  'Pass jwt.bearer(ctx.req) directly as the second jwt.verify argument.'
1142
1279
  ));
1143
1280
  }
1144
- for (const entry of entries) validateResultAccess(ts, sourceFile, entry, diagnostics);
1281
+ for (const entry of entries.filter(entry => entry.kind === 'jwt-verify')) validateResultAccess(ts, sourceFile, entry, diagnostics);
1145
1282
 
1146
1283
  const canonicalEffects = entries.map((entry) => entry.canonicalEffect);
1147
1284
  const canonicalIntrinsics = entries.map((entry) => entry.bearerIntrinsic).filter(Boolean);
1148
1285
  const schemaReferences = entries.map((entry) => entry.schemaReference).filter(Boolean);
1149
- const cryptoRequirements = jwtContracts.jwtCryptoRequirements(
1286
+ const cryptoRequirements = Object.freeze([...jwtContracts.jwtCryptoRequirements(
1150
1287
  entries.flatMap((entry) => entry.algorithms)
1151
- );
1288
+ ), ...[...new Set(entries.filter(entry => entry.kind === 'jwt-sign').map(entry =>
1289
+ entry.signingAlgorithm === 'HS256' ? 'HMAC-SHA256' : entry.signingAlgorithm))].map(algorithm => ({
1290
+ version: packageContracts.PACKAGE_CRYPTO_REQUIREMENT_VERSION, requestedBy: jwtContracts.JWT_PACKAGE_NAME,
1291
+ semanticOwner: '@pulse-compute/crypto', reachable: true, algorithms: [algorithm]
1292
+ }))]);
1152
1293
  const keyArtifacts = entries
1153
1294
  .filter((entry) => entry.key.keyArtifactId)
1154
1295
  .map((entry) => Object.freeze({
@@ -1161,7 +1302,7 @@ function buildJwtLoweringPlan(inputs = {}) {
1161
1302
  const uniqueKeyArtifacts = [...new Map(keyArtifacts.map((entry) => [entry.id, entry])).values()];
1162
1303
  const realizationArtifacts = [...new Map(privateRealizationArtifacts
1163
1304
  .map((entry) => [entry.id, entry])).values()];
1164
- const guestUnits = entries.some((entry) => entry.algorithms.includes('ES256'))
1305
+ const guestUnits = entries.some((entry) => entry.algorithms.some(algorithm => ['ES256', 'RS256'].includes(algorithm)) || ['ES256', 'RS256'].includes(entry.signingAlgorithm))
1165
1306
  ? Object.freeze([loadCryptoGuestContribution()])
1166
1307
  : Object.freeze([]);
1167
1308
  const artifact = deepFreeze(normalizeArtifact({
@@ -9,7 +9,7 @@ const pulseWasmManifest = Object.freeze({
9
9
  facade: Object.freeze({
10
10
  namespace: 'jwt',
11
11
  import: '@pulse-compute/jwt',
12
- symbols: Object.freeze(['bearer', 'verify'])
12
+ symbols: Object.freeze(['bearer', 'verify', 'sign'])
13
13
  }),
14
14
  publicApi: Object.freeze({
15
15
  package: '@pulse-compute/jwt',
@@ -18,12 +18,13 @@ const pulseWasmManifest = Object.freeze({
18
18
  'JwtError',
19
19
  'bearer',
20
20
  'jwt',
21
- 'verify'
21
+ 'verify',
22
+ 'sign'
22
23
  ]),
23
24
  compatibilitySubpaths: Object.freeze([]),
24
25
  loweringIntentionallyNarrow: true
25
26
  }),
26
- description: 'Package-owned Native lowering contract for bounded, provider-neutral JWT verification.',
27
+ description: 'Package-owned Native lowering contract for bounded, provider-neutral JWT verification and HS256 signing.',
27
28
  compiler: Object.freeze({
28
29
  version: 'pulsewasm.lowerable-compiler-builder.v1',
29
30
  entry: './pulsewasm.compiler.cjs',
@@ -63,6 +64,7 @@ const pulseWasmManifest = Object.freeze({
63
64
  ])
64
65
  }),
65
66
  lowerings: Object.freeze([
67
+ Object.freeze({ tsSymbol: 'jwt.sign', asSymbol: 'pulse_jwt_sign', callShape: 'claims-static-sign-policy', hostCapabilities: Object.freeze(['clock', 'result']) }),
66
68
  Object.freeze({
67
69
  tsSymbol: 'jwt.verify',
68
70
  asSymbol: 'pulse_jwt_verify',
@@ -85,11 +87,11 @@ const pulseWasmManifest = Object.freeze({
85
87
  realization: 'guest-source:pulse-hmac-as',
86
88
  implementation: 'pulse-hmac-as.v1',
87
89
  packageSource: 'pulse-jwt-as',
88
- algorithms: Object.freeze(['HS256', 'ES256']),
90
+ algorithms: Object.freeze(['HS256', 'ES256', 'RS256']),
89
91
  keyTypes: Object.freeze(['secret', 'jwk', 'jwks']),
90
92
  guestUnitRequired: false,
91
93
  guestUnits: Object.freeze({
92
- ES256: 'pulse.crypto.es256.rustcrypto-p256.v1'
94
+ ES256: 'pulse.crypto.es256.rustcrypto-p256.v1', RS256: 'pulse.crypto.es256.rustcrypto-p256.v1'
93
95
  }),
94
96
  portable: true,
95
97
  automaticFallback: false
@@ -13,8 +13,8 @@ function sha256(value) {
13
13
  return crypto.createHash('sha256').update(value).digest('hex');
14
14
  }
15
15
 
16
- function pulseJwtAssemblyScriptSource() {
17
- const source = fs.readFileSync(sourceFile, 'utf8');
16
+ function pulseJwtAssemblyScriptSource(options = {}) {
17
+ const source = fs.readFileSync(sourceFile, 'utf8') + (options.includeSigning === true ? '\n' + fs.readFileSync(path.join(__dirname, 'as/sign.as.ts'), 'utf8') : '');
18
18
  return Object.freeze({
19
19
  version: JWT_NATIVE_SOURCE_CONTRACT_VERSION,
20
20
  id: 'pulse-jwt-as',
@@ -26,13 +26,16 @@ function pulseJwtAssemblyScriptSource() {
26
26
  origin: 'package-source',
27
27
  sourceIncluded: true,
28
28
  sourceFile: sourceRelativeFile,
29
+ sourceFiles: Object.freeze([sourceRelativeFile, ...(options.includeSigning === true ? ['as/sign.as.ts'] : [])]),
29
30
  source,
30
31
  sourceBytes: Buffer.byteLength(source),
31
32
  sourceSha256: sha256(source),
32
33
  imports: Object.freeze([]),
33
34
  exports: Object.freeze([
34
35
  'pulse_jwt_verify',
35
- 'pulse_jwt_fastly_verify'
36
+ 'pulse_jwt_fastly_verify',
37
+ 'pulse_jwt_sign',
38
+ ...(options.includeSigning === true ? ['pulse_jwt_fastly_sign'] : [])
36
39
  ]),
37
40
  semanticOwnership: Object.freeze([
38
41
  'compact-jws-parsing',