@spfn/auth 0.2.0-beta.87 → 0.2.0-beta.89

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 (50) hide show
  1. package/README.md +384 -18
  2. package/dist/{authenticate-C7w-pLOR.d.ts → authenticate-DK-hLSOW.d.ts} +154 -5
  3. package/dist/client-proof.d.ts +122 -2
  4. package/dist/client-proof.js +366 -5
  5. package/dist/client-proof.js.map +1 -1
  6. package/dist/config.d.ts +36 -0
  7. package/dist/config.js +18 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/errors.d.ts +93 -2
  10. package/dist/errors.js +57 -0
  11. package/dist/errors.js.map +1 -1
  12. package/dist/index.d.ts +32 -3
  13. package/dist/index.js +60 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/nextjs/server.d.ts +2 -2
  16. package/dist/server.d.ts +126 -11
  17. package/dist/server.js +527 -106
  18. package/dist/server.js.map +1 -1
  19. package/dist/{session-CGxgH3C9.d.ts → session-CFK4BT25.d.ts} +1 -1
  20. package/dist/{types-1BMx0OX1.d.ts → types-CD95yudz.d.ts} +15 -1
  21. package/migrations/20251125021229_premium_famine/snapshot.json +2641 -0
  22. package/migrations/20260225130050_smooth_the_fury/snapshot.json +2686 -0
  23. package/migrations/20260308141417_deep_iceman/snapshot.json +2686 -0
  24. package/migrations/20260308151309_perfect_deathbird/snapshot.json +2731 -0
  25. package/migrations/20260308201135_concerned_rawhide_kid/snapshot.json +2786 -0
  26. package/migrations/20260629103209_lethal_lifeguard/snapshot.json +2786 -0
  27. package/migrations/20260709073531_easy_hardball/snapshot.json +3119 -0
  28. package/migrations/20260714081434_glossy_major_mapleleaf/snapshot.json +3112 -0
  29. package/migrations/20260804105939_amazing_bushwacker/migration.sql +3 -0
  30. package/migrations/20260804105939_amazing_bushwacker/snapshot.json +3112 -0
  31. package/migrations/20260804110033_fat_piledriver/migration.sql +2 -0
  32. package/migrations/20260804110033_fat_piledriver/snapshot.json +3138 -0
  33. package/package.json +3 -6
  34. package/migrations/meta/0000_snapshot.json +0 -1632
  35. package/migrations/meta/0001_snapshot.json +0 -1660
  36. package/migrations/meta/0002_snapshot.json +0 -1660
  37. package/migrations/meta/0003_snapshot.json +0 -1689
  38. package/migrations/meta/0004_snapshot.json +0 -1721
  39. package/migrations/meta/0005_snapshot.json +0 -1721
  40. package/migrations/meta/0006_snapshot.json +0 -1921
  41. package/migrations/meta/0007_snapshot.json +0 -1916
  42. package/migrations/meta/_journal.json +0 -62
  43. /package/migrations/{0000_premium_famine.sql → 20251125021229_premium_famine/migration.sql} +0 -0
  44. /package/migrations/{0001_smooth_the_fury.sql → 20260225130050_smooth_the_fury/migration.sql} +0 -0
  45. /package/migrations/{0002_deep_iceman.sql → 20260308141417_deep_iceman/migration.sql} +0 -0
  46. /package/migrations/{0003_perfect_deathbird.sql → 20260308151309_perfect_deathbird/migration.sql} +0 -0
  47. /package/migrations/{0004_concerned_rawhide_kid.sql → 20260308201135_concerned_rawhide_kid/migration.sql} +0 -0
  48. /package/migrations/{0005_lethal_lifeguard.sql → 20260629103209_lethal_lifeguard/migration.sql} +0 -0
  49. /package/migrations/{0006_easy_hardball.sql → 20260709073531_easy_hardball/migration.sql} +0 -0
  50. /package/migrations/{0007_glossy_major_mapleleaf.sql → 20260714081434_glossy_major_mapleleaf/migration.sql} +0 -0
@@ -496,6 +496,18 @@ var ClientProofRefusal = class _ClientProofRefusal {
496
496
  static unprocessable() {
497
497
  return contractViolation("the request could not be processed");
498
498
  }
499
+ /**
500
+ * A client that ships separately from the server said nothing about which
501
+ * contract it was built against. Without it the server cannot tell whether
502
+ * the two ends agree, and answering as though they do is what produces the
503
+ * undecodable body this check exists to replace.
504
+ */
505
+ static contractVersionMissing() {
506
+ return contractViolation("a client of this kind must state the contract version it was generated from");
507
+ }
508
+ static contractVersionUnsupported() {
509
+ return contractViolation("the stated contract version is outside the range this server serves");
510
+ }
499
511
  // ---- the profile allowlist ----------------------------------------------
500
512
  static profileRejected() {
501
513
  return new _ClientProofRefusal("PROFILE_REJECTED", "the named auth profile is not on this contract's allowlist");
@@ -983,6 +995,36 @@ var AUTH_SURFACE_OPERATIONS = [
983
995
  requestType: "RotateKeyRequest",
984
996
  responseType: "RotateKeyResponse",
985
997
  summary: "Replaces the authenticated key with a new client-generated public key before its TTL runs out."
998
+ },
999
+ {
1000
+ id: "auth.keys.list",
1001
+ method: "POST",
1002
+ path: "/_auth/keys/list",
1003
+ authProfile: "clientProofV1",
1004
+ requiresSession: false,
1005
+ requestType: "ListKeysRequest",
1006
+ responseType: "ListKeysResponse",
1007
+ summary: "Lists the keys registered to the caller, one per device that can sign for them."
1008
+ },
1009
+ {
1010
+ id: "auth.keys.revoke",
1011
+ method: "POST",
1012
+ path: "/_auth/keys/revoke",
1013
+ authProfile: "clientProofV1",
1014
+ requiresSession: false,
1015
+ requestType: "RevokeKeyRequest",
1016
+ responseType: "RevokeKeyResponse",
1017
+ summary: "Revokes one of the caller's keys, signing that device out."
1018
+ },
1019
+ {
1020
+ id: "auth.keys.revokeAll",
1021
+ method: "POST",
1022
+ path: "/_auth/keys/revoke-all",
1023
+ authProfile: "clientProofV1",
1024
+ requiresSession: false,
1025
+ requestType: "RevokeAllKeysRequest",
1026
+ responseType: "RevokeAllKeysResponse",
1027
+ summary: "Revokes every key the caller has, sparing the calling device unless asked otherwise."
986
1028
  }
987
1029
  ];
988
1030
  var ContractTypeError = class extends Error {
@@ -1015,17 +1057,17 @@ function decodeListItemsRequest(value) {
1015
1057
  }
1016
1058
  return request;
1017
1059
  }
1018
- function objectWithKeys(value, required, optional) {
1060
+ function objectWithKeys(value, required2, optional2) {
1019
1061
  if (!(value instanceof Map)) {
1020
1062
  throw new ContractTypeError();
1021
1063
  }
1022
- for (const key of required) {
1064
+ for (const key of required2) {
1023
1065
  if (!value.has(key)) {
1024
1066
  throw new ContractTypeError();
1025
1067
  }
1026
1068
  }
1027
1069
  for (const key of value.keys()) {
1028
- if (!required.includes(key) && !optional.includes(key)) {
1070
+ if (!required2.includes(key) && !optional2.includes(key)) {
1029
1071
  throw new ContractTypeError();
1030
1072
  }
1031
1073
  }
@@ -1221,6 +1263,289 @@ function answer(status, value) {
1221
1263
  return new Response(buffer, { status, headers: { "content-type": "application/json" } });
1222
1264
  }
1223
1265
 
1266
+ // src/server/client-proof/contract-bundle.ts
1267
+ import { createHash as createHash2 } from "crypto";
1268
+
1269
+ // src/server/types.ts
1270
+ var KEY_ALGORITHM = ["ES256", "RS256"];
1271
+
1272
+ // src/server/client-proof/wire-headers.ts
1273
+ var CLIENT_IDENTITY_HEADERS = {
1274
+ kind: "x-spfn-client-kind",
1275
+ version: "x-spfn-client-version",
1276
+ contractVersion: "x-spfn-client-contract-version"
1277
+ };
1278
+ var SERVER_CONTRACT_HEADERS = {
1279
+ version: "x-spfn-server-contract-version",
1280
+ supportedRange: "x-spfn-supported-contract-range"
1281
+ };
1282
+ var CLIENT_KINDS = ["web", "ios", "android"];
1283
+ function isAppKind(kind) {
1284
+ return kind !== "web";
1285
+ }
1286
+
1287
+ // src/server/client-proof/contract-bundle.ts
1288
+ var CONTRACT_VERSION = "0.6.0";
1289
+ var CONTRACT_MAJOR = 0;
1290
+ var CONTRACT_SUPPORTED_RANGE = ">=0.6.0 <0.7.0";
1291
+ function required(name, type) {
1292
+ return { name, type, optional: false };
1293
+ }
1294
+ function optional(name, type) {
1295
+ return { name, type, optional: true };
1296
+ }
1297
+ var CONTRACT_TYPES = [
1298
+ {
1299
+ name: "HandshakeRequest",
1300
+ fields: [
1301
+ required("clientId", "string"),
1302
+ required("keyId", "string"),
1303
+ required("nonce", "string"),
1304
+ required("issuedAtMillis", "integer")
1305
+ ]
1306
+ },
1307
+ {
1308
+ name: "HandshakeResponse",
1309
+ fields: [
1310
+ required("sessionId", "string"),
1311
+ required("expiresAtMillis", "integer")
1312
+ ]
1313
+ },
1314
+ {
1315
+ name: "EchoRequest",
1316
+ fields: [
1317
+ required("message", "string"),
1318
+ required("sequence", "integer")
1319
+ ]
1320
+ },
1321
+ {
1322
+ name: "EchoResponse",
1323
+ fields: [
1324
+ required("message", "string"),
1325
+ required("sequence", "integer"),
1326
+ required("serverTimeMillis", "integer")
1327
+ ]
1328
+ },
1329
+ {
1330
+ name: "ListItemsRequest",
1331
+ fields: [
1332
+ required("limit", "integer"),
1333
+ optional("cursor", "string")
1334
+ ]
1335
+ },
1336
+ {
1337
+ name: "Item",
1338
+ fields: [
1339
+ required("id", "string"),
1340
+ required("name", "string"),
1341
+ required("updatedAtMillis", "integer")
1342
+ ]
1343
+ },
1344
+ {
1345
+ name: "ListItemsResponse",
1346
+ fields: [
1347
+ required("items", "array<Item>"),
1348
+ optional("nextCursor", "string")
1349
+ ]
1350
+ },
1351
+ {
1352
+ name: "RegisterRequest",
1353
+ fields: [
1354
+ optional("email", "string"),
1355
+ optional("phone", "string"),
1356
+ required("verificationToken", "string"),
1357
+ required("password", "string"),
1358
+ required("publicKey", "string"),
1359
+ required("keyId", "string"),
1360
+ required("fingerprint", "string"),
1361
+ required("algorithm", "KeyAlgorithm")
1362
+ ]
1363
+ },
1364
+ {
1365
+ name: "RegisterResponse",
1366
+ fields: [
1367
+ required("userId", "string"),
1368
+ required("publicId", "string"),
1369
+ optional("email", "string"),
1370
+ optional("phone", "string")
1371
+ ]
1372
+ },
1373
+ {
1374
+ name: "LoginRequest",
1375
+ fields: [
1376
+ optional("email", "string"),
1377
+ optional("phone", "string"),
1378
+ required("password", "string"),
1379
+ required("publicKey", "string"),
1380
+ required("keyId", "string"),
1381
+ required("fingerprint", "string"),
1382
+ required("algorithm", "KeyAlgorithm"),
1383
+ optional("oldKeyId", "string")
1384
+ ]
1385
+ },
1386
+ {
1387
+ name: "LoginResponse",
1388
+ fields: [
1389
+ required("userId", "string"),
1390
+ required("publicId", "string"),
1391
+ optional("email", "string"),
1392
+ optional("phone", "string"),
1393
+ required("passwordChangeRequired", "boolean")
1394
+ ]
1395
+ },
1396
+ {
1397
+ name: "OauthNativeRequest",
1398
+ fields: [
1399
+ required("idToken", "string"),
1400
+ required("nonce", "string"),
1401
+ optional("accessToken", "string"),
1402
+ required("publicKey", "string"),
1403
+ required("keyId", "string"),
1404
+ required("fingerprint", "string"),
1405
+ required("algorithm", "KeyAlgorithm")
1406
+ ]
1407
+ },
1408
+ {
1409
+ name: "OauthNativeResponse",
1410
+ fields: [
1411
+ required("userId", "string"),
1412
+ required("keyId", "string"),
1413
+ required("isNewUser", "boolean")
1414
+ ]
1415
+ },
1416
+ {
1417
+ name: "RotateKeyRequest",
1418
+ fields: [
1419
+ required("publicKey", "string"),
1420
+ required("keyId", "string"),
1421
+ required("fingerprint", "string"),
1422
+ required("algorithm", "KeyAlgorithm")
1423
+ ]
1424
+ },
1425
+ {
1426
+ name: "RotateKeyResponse",
1427
+ fields: [
1428
+ required("success", "boolean"),
1429
+ required("keyId", "string")
1430
+ ]
1431
+ },
1432
+ {
1433
+ name: "ListKeysRequest",
1434
+ fields: [
1435
+ optional("includeRevoked", "boolean")
1436
+ ]
1437
+ },
1438
+ {
1439
+ name: "KeySummary",
1440
+ fields: [
1441
+ required("keyId", "string"),
1442
+ optional("deviceName", "string"),
1443
+ optional("platform", "string"),
1444
+ required("algorithm", "KeyAlgorithm"),
1445
+ required("fingerprintPrefix", "string"),
1446
+ required("createdAtMillis", "integer"),
1447
+ optional("lastUsedAtMillis", "integer"),
1448
+ optional("expiresAtMillis", "integer"),
1449
+ required("isExpired", "boolean"),
1450
+ required("isActive", "boolean"),
1451
+ optional("revokedAtMillis", "integer")
1452
+ ]
1453
+ },
1454
+ {
1455
+ name: "ListKeysResponse",
1456
+ fields: [
1457
+ required("keys", "array<KeySummary>")
1458
+ ]
1459
+ },
1460
+ {
1461
+ name: "RevokeKeyRequest",
1462
+ fields: [
1463
+ required("keyId", "string")
1464
+ ]
1465
+ },
1466
+ {
1467
+ name: "RevokeKeyResponse",
1468
+ fields: [
1469
+ required("keyId", "string"),
1470
+ required("selfRevoked", "boolean")
1471
+ ]
1472
+ },
1473
+ {
1474
+ name: "RevokeAllKeysRequest",
1475
+ fields: [
1476
+ optional("includeCurrent", "boolean")
1477
+ ]
1478
+ },
1479
+ {
1480
+ name: "RevokeAllKeysResponse",
1481
+ fields: [
1482
+ required("revokedCount", "integer"),
1483
+ required("currentKeyRevoked", "boolean")
1484
+ ]
1485
+ }
1486
+ ];
1487
+ var CONTRACT_ENUMS = [
1488
+ { name: "KeyAlgorithm", values: [...KEY_ALGORITHM] }
1489
+ ];
1490
+ var BUNDLE_FILENAME = "spfn-mobile-contract.json";
1491
+ var BUNDLE_REPO_PATH = `contracts/mobile/${BUNDLE_FILENAME}`;
1492
+
1493
+ // src/server/client-proof/wire-version.ts
1494
+ function readClientIdentity(headers) {
1495
+ const kind = headers.get(CLIENT_IDENTITY_HEADERS.kind);
1496
+ if (kind === null || !isClientKind(kind)) {
1497
+ return null;
1498
+ }
1499
+ return {
1500
+ kind,
1501
+ version: headers.get(CLIENT_IDENTITY_HEADERS.version),
1502
+ contractVersion: headers.get(CLIENT_IDENTITY_HEADERS.contractVersion)
1503
+ };
1504
+ }
1505
+ function isClientKind(value) {
1506
+ return CLIENT_KINDS.includes(value);
1507
+ }
1508
+ function isContractVersionSupported(clientVersion) {
1509
+ const client = parseVersion(clientVersion);
1510
+ if (client === null) {
1511
+ return false;
1512
+ }
1513
+ const server = parseVersion(CONTRACT_VERSION);
1514
+ if (server === null || client.major !== server.major) {
1515
+ return false;
1516
+ }
1517
+ return CONTRACT_MAJOR > 0 || client.minor === server.minor;
1518
+ }
1519
+ function parseVersion(raw) {
1520
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(raw);
1521
+ if (match === null) {
1522
+ return null;
1523
+ }
1524
+ return { major: Number(match[1]), minor: Number(match[2]) };
1525
+ }
1526
+ function judgeClientIdentity(identity) {
1527
+ if (identity === null || !isAppKind(identity.kind)) {
1528
+ return null;
1529
+ }
1530
+ if (identity.contractVersion === null) {
1531
+ return ClientProofRefusal.contractVersionMissing();
1532
+ }
1533
+ if (!isContractVersionSupported(identity.contractVersion)) {
1534
+ return ClientProofRefusal.contractVersionUnsupported();
1535
+ }
1536
+ return null;
1537
+ }
1538
+ function applyServerContractHeaders(headers) {
1539
+ headers.set(SERVER_CONTRACT_HEADERS.version, CONTRACT_VERSION);
1540
+ headers.set(SERVER_CONTRACT_HEADERS.supportedRange, CONTRACT_SUPPORTED_RANGE);
1541
+ }
1542
+ function serverContractHeaders() {
1543
+ return {
1544
+ [SERVER_CONTRACT_HEADERS.version]: CONTRACT_VERSION,
1545
+ [SERVER_CONTRACT_HEADERS.supportedRange]: CONTRACT_SUPPORTED_RANGE
1546
+ };
1547
+ }
1548
+
1224
1549
  // src/server/client-proof/dev-handler.ts
1225
1550
  var MAX_BODY_BYTES = 1 << 20;
1226
1551
  var HTTP_OK2 = 200;
@@ -1338,7 +1663,7 @@ function listItems(request) {
1338
1663
  function contractResponse(status, body) {
1339
1664
  return new Response(toArrayBuffer(body), {
1340
1665
  status,
1341
- headers: { "content-type": "application/json" }
1666
+ headers: { "content-type": "application/json", ...serverContractHeaders() }
1342
1667
  });
1343
1668
  }
1344
1669
  async function readBodyCapped(request) {
@@ -1389,7 +1714,8 @@ function createClientProofGuard(state, options = {}) {
1389
1714
  const bytes = admission.refusal.envelopeBytes(newHexId());
1390
1715
  const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1391
1716
  return c.newResponse(buffer, admission.refusal.httpStatus, {
1392
- "content-type": "application/json"
1717
+ "content-type": "application/json",
1718
+ ...serverContractHeaders()
1393
1719
  });
1394
1720
  }
1395
1721
  c.set("clientType", "mobile");
@@ -1401,9 +1727,36 @@ function createClientProofGuard(state, options = {}) {
1401
1727
  return void 0;
1402
1728
  };
1403
1729
  }
1730
+
1731
+ // src/server/client-proof/version-middleware.ts
1732
+ var CLIENT_IDENTITY_CONTEXT_KEY = "clientIdentity";
1733
+ function createClientVersionMiddleware() {
1734
+ return async (c, next) => {
1735
+ const identity = readClientIdentity(c.req.raw.headers);
1736
+ const refusal = judgeClientIdentity(identity);
1737
+ if (refusal !== null) {
1738
+ const bytes = refusal.envelopeBytes(newHexId());
1739
+ const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1740
+ const response = c.newResponse(buffer, refusal.httpStatus, {
1741
+ "content-type": "application/json"
1742
+ });
1743
+ applyServerContractHeaders(response.headers);
1744
+ return response;
1745
+ }
1746
+ if (identity !== null) {
1747
+ c.set(CLIENT_IDENTITY_CONTEXT_KEY, identity);
1748
+ }
1749
+ await next();
1750
+ applyServerContractHeaders(c.res.headers);
1751
+ return void 0;
1752
+ };
1753
+ }
1404
1754
  export {
1405
1755
  ABSENT_BODY_SHA256,
1406
1756
  AUTH_SURFACE_OPERATIONS,
1757
+ CLIENT_IDENTITY_CONTEXT_KEY,
1758
+ CLIENT_IDENTITY_HEADERS,
1759
+ CLIENT_KINDS,
1407
1760
  CLIENT_PROOF_CONTENT_TYPE,
1408
1761
  CLIENT_PROOF_HEADERS,
1409
1762
  CLIENT_PROOF_PROFILE,
@@ -1424,12 +1777,15 @@ export {
1424
1777
  PROOF_SIGNATURE_HEX_LENGTH,
1425
1778
  ProofInputError,
1426
1779
  RedisReplayStore,
1780
+ SERVER_CONTRACT_HEADERS,
1427
1781
  TestClock,
1428
1782
  admitClientProofRequest,
1783
+ applyServerContractHeaders,
1429
1784
  canonicalProofInput,
1430
1785
  configureClientProofReplayStore,
1431
1786
  createClientProofDevHandler,
1432
1787
  createClientProofGuard,
1788
+ createClientVersionMiddleware,
1433
1789
  decodeEchoRequest,
1434
1790
  decodeHandshakeRequest,
1435
1791
  decodeListItemsRequest,
@@ -1438,13 +1794,18 @@ export {
1438
1794
  encodeHandshakeResponse,
1439
1795
  encodeListItemsResponse,
1440
1796
  getClientProofReplayStore,
1797
+ isAppKind,
1441
1798
  isCanonicalBytes,
1799
+ isContractVersionSupported,
1442
1800
  isRequestContentType,
1801
+ judgeClientIdentity,
1443
1802
  newHexId,
1444
1803
  parseCanonicalJson,
1445
1804
  parseClientProofPublicKey,
1805
+ readClientIdentity,
1446
1806
  readCredentials,
1447
1807
  replayLedgerKey,
1808
+ serverContractHeaders,
1448
1809
  sha256Hex,
1449
1810
  signClientProof,
1450
1811
  systemClock,