@byok-sdk/core 0.29.0 → 0.30.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.
@@ -13,3 +13,11 @@ export { deriveScopeKeyId, generateScopeKey, openScopeKeyPackage, sealScopeKeyPa
13
13
  export type { OpenScopeKeyPackageInput, ScopeKeyPackageOpenResult, ScopeKeyPackageV1, SealScopeKeyPackageInput, } from './scope-key';
14
14
  export { MEMBER_CHAIN_OPS, deriveMemberId, memberChainEntryHash, signMemberChainEntry, verifyMemberChainEntry, } from './member-chain';
15
15
  export type { MemberChainEntryV1, MemberChainEntryVerifyResult, MemberChainOp, UnsignedMemberChainEntryV1, } from './member-chain';
16
+ export { MEMBER_KEY_BUNDLE_VERSION, createMemberKeyBundle, parseMemberKeyBundle, verifyMemberKem } from './member-bundle';
17
+ export type { CreateMemberKeyBundleInput, MemberKeyBundleV1 } from './member-bundle';
18
+ export { MEMBER_FINGERPRINT_DOMAIN, memberApprovalCode, memberFingerprint } from './member-fingerprint';
19
+ export type { MemberFingerprintInput } from './member-fingerprint';
20
+ export { CLIENT_PAIRING_MAC_DOMAIN, DEVICE_PAIRING_MAC_DOMAIN, PAIRING_VERIFICATION_SECRET_BYTES, computeClientPairingMac, computeDevicePairingMac, decodePairingVerificationSecret, encodePairingVerificationSecret, formatPairingInput, generatePairingVerificationSecret, parsePairingInput, verifyClientPairingMac, verifyDevicePairingMac, } from './pairing-verification';
21
+ export type { ClientPairingMacInput, DevicePairingMacInput, PairingInput } from './pairing-verification';
22
+ export { applyMemberChainEntries, emptyMemberChainState, memberChainScopeRecipients, verifyMemberChainFromAnchor } from './member-chain-walk';
23
+ export type { MemberChainAnchorFailure, MemberChainAnchorResult, MemberChainHead, MemberChainScope, MemberChainState, MemberChainWalkFailure, MemberChainWalkResult, VerifyMemberChainFromAnchorInput, } from './member-chain-walk';
@@ -137,6 +137,15 @@ function isCanonicalTimestamp(value) {
137
137
  if (!Number.isFinite(parsed)) return false;
138
138
  return new Date(parsed).toISOString() === value;
139
139
  }
140
+
141
+ // src/total-parse.ts
142
+ function safeParseTotal(schema, value) {
143
+ try {
144
+ return schema.safeParse(value);
145
+ } catch {
146
+ return { success: false };
147
+ }
148
+ }
140
149
  function webCryptoSubtle() {
141
150
  const subtle = globalThis.crypto?.subtle;
142
151
  if (subtle === void 0) {
@@ -388,7 +397,7 @@ var SealingPublicJwkSchema = z.strictObject({
388
397
  });
389
398
  var SealingKeyIdSchema = z.string().regex(new RegExp(`^[A-Za-z0-9_-]{${SEALING_KEY_ID_LENGTH}}$`));
390
399
  function parseSealingPublicJwk(input) {
391
- const result = SealingPublicJwkSchema.safeParse(input);
400
+ const result = safeParseTotal(SealingPublicJwkSchema, input);
392
401
  if (!result.success) {
393
402
  throw new ByokCoreError("sealed_secret_invalid", "Sealing public key must be a strict P-256 EC public JWK");
394
403
  }
@@ -597,15 +606,16 @@ var MEMBER_ID_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
597
606
  var MEMBER_CHAIN_VERSION = 1;
598
607
  var MEMBER_CHAIN_DOMAIN_PREFIX = "byok-member-chain-v1\n";
599
608
  var MEMBER_CHAIN_OPS = ["add", "remove", "rotate", "scope-policy", "shred"];
600
- var CanonicalJsonObjectSchema = z.custom((value) => {
601
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
609
+ var CanonicalJsonObjectSchema = z.unknown().transform((value, ctx) => {
602
610
  try {
603
- canonicalizeJson(value);
604
- return true;
611
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
612
+ return JSON.parse(canonicalizeJson(value));
613
+ }
605
614
  } catch {
606
- return false;
607
615
  }
608
- }, "must be a canonical JSON object");
616
+ ctx.addIssue({ code: "custom", message: "must be a canonical JSON object" });
617
+ return z.NEVER;
618
+ });
609
619
  var unsignedEntryShape = {
610
620
  v: z.literal(MEMBER_CHAIN_VERSION),
611
621
  tenantId: z.string().min(1),
@@ -627,7 +637,7 @@ async function deriveMemberId(signingPublicJwk) {
627
637
  return encodeBase64Url(await sha256(concat2(utf82(MEMBER_ID_DOMAIN), raw)));
628
638
  }
629
639
  function parseUnsignedEntry(entry) {
630
- const parsed = UnsignedMemberChainEntryV1Schema.safeParse(entry);
640
+ const parsed = safeParseTotal(UnsignedMemberChainEntryV1Schema, entry);
631
641
  if (!parsed.success) throw invalidInput("Member chain entry is invalid");
632
642
  return parsed.data;
633
643
  }
@@ -659,7 +669,7 @@ async function signMemberChainEntry(entry, signer) {
659
669
  return { ...unsigned, sig: await ecdsaSign(signer.publicJwk, signer.privateKey, signingInput(unsigned)) };
660
670
  }
661
671
  async function verifyMemberChainEntry(entry, signerPublicJwk) {
662
- const parsed = MemberChainEntryV1Schema.safeParse(entry);
672
+ const parsed = safeParseTotal(MemberChainEntryV1Schema, entry);
663
673
  if (!parsed.success || await deriveMemberId(signerPublicJwk) !== parsed.data.signer) {
664
674
  return { ok: false, reason: "member_chain_entry_invalid" };
665
675
  }
@@ -741,7 +751,7 @@ function packageSigningInput(pkg) {
741
751
  }
742
752
  var PackageIdentitySchema = ScopeKeyPackageV1Schema.omit({ enc: true, ciphertext: true, sig: true });
743
753
  async function sealScopeKeyPackage(input) {
744
- const parsed = PackageIdentitySchema.safeParse({
754
+ const parsed = safeParseTotal(PackageIdentitySchema, {
745
755
  v: SCOPE_KEY_PACKAGE_VERSION,
746
756
  tenantId: input.tenantId,
747
757
  scopeId: input.scopeId,
@@ -762,7 +772,7 @@ async function sealScopeKeyPackage(input) {
762
772
  return { ...unsigned, sig: await ecdsaSign(input.sender.signingPublicJwk, input.sender.signingPrivateKey, packageSigningInput(unsigned)) };
763
773
  }
764
774
  async function openScopeKeyPackage(input) {
765
- const parsed = ScopeKeyPackageV1Schema.safeParse(input.package);
775
+ const parsed = safeParseTotal(ScopeKeyPackageV1Schema, input.package);
766
776
  if (!parsed.success) return { ok: false, reason: "key_package_invalid" };
767
777
  const pkg = parsed.data;
768
778
  if (pkg.recipient !== input.recipient.memberId || pkg.tenantId !== input.expected.tenantId || pkg.scopeId !== input.expected.scopeId || pkg.epoch !== input.expected.epoch || pkg.sender !== await deriveMemberId(input.senderSigningPublicJwk)) {
@@ -792,6 +802,39 @@ async function openScopeKeyPackage(input) {
792
802
  return { ok: true, scopeKey, keyId: pkg.keyId };
793
803
  }
794
804
 
805
+ // src/sealed-payload/sealed-json.ts
806
+ function canonicalValue(value, seen) {
807
+ if (value === null) return "null";
808
+ switch (typeof value) {
809
+ case "boolean":
810
+ return value ? "true" : "false";
811
+ case "string":
812
+ return JSON.stringify(value);
813
+ case "number":
814
+ if (!Number.isFinite(value)) throw invalidInput("Sealed plaintext holds a number with no JSON form");
815
+ return String(value);
816
+ case "object":
817
+ break;
818
+ default:
819
+ throw invalidInput(`Sealed plaintext holds a ${typeof value}, which is not a JSON type`);
820
+ }
821
+ if (seen.has(value)) throw invalidInput("Sealed plaintext holds a circular reference");
822
+ seen.add(value);
823
+ try {
824
+ if (Array.isArray(value)) return `[${Array.from(value, (entry) => canonicalValue(entry, seen)).join(",")}]`;
825
+ const prototype = Object.getPrototypeOf(value);
826
+ if (prototype !== Object.prototype && prototype !== null) throw invalidInput("Sealed plaintext holds an object that is not plain");
827
+ const record = value;
828
+ const members = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalValue(record[key], seen)}`);
829
+ return `{${members.join(",")}}`;
830
+ } finally {
831
+ seen.delete(value);
832
+ }
833
+ }
834
+ function canonicalizeSealedJson(value) {
835
+ return canonicalValue(value, /* @__PURE__ */ new Set());
836
+ }
837
+
795
838
  // src/sealed-payload/sealed-content.ts
796
839
  var SEALED_CONTENT_VERSION = 1;
797
840
  var SEALED_CONTENT_SUITE = "byok-sealed-v1.hkdf-sha256.aes-256-gcm";
@@ -890,7 +933,7 @@ var SealedBindingSchema = z.strictObject({
890
933
  }
891
934
  });
892
935
  function parseBinding(binding) {
893
- const parsed = SealedBindingSchema.safeParse(binding);
936
+ const parsed = safeParseTotal(SealedBindingSchema, binding);
894
937
  if (!parsed.success) throw invalidInput("Sealed content binding is invalid");
895
938
  return parsed.data;
896
939
  }
@@ -899,8 +942,8 @@ function assertMaxContentLength(maxContentLength) {
899
942
  throw invalidInput("Sealed content limit must be a non-negative 32-bit integer");
900
943
  }
901
944
  }
902
- function aadBytes(header, binding) {
903
- return canonicalizeJsonBytes({
945
+ function bindingAad(header, binding) {
946
+ return {
904
947
  v: header.v,
905
948
  suite: header.suite,
906
949
  category: header.category,
@@ -916,10 +959,11 @@ function aadBytes(header, binding) {
916
959
  deviceId: binding.deviceId,
917
960
  agentRef: binding.agentRef === null ? null : { agentId: binding.agentRef.agentId, profileRevision: binding.agentRef.profileRevision },
918
961
  categories: [...binding.categories],
919
- notAfter: binding.notAfter,
920
- paddedLength: header.paddedLength,
921
- contentMac: header.contentMac
922
- });
962
+ notAfter: binding.notAfter
963
+ };
964
+ }
965
+ function aadBytes(header, binding) {
966
+ return canonicalizeJsonBytes({ ...bindingAad(header, binding), paddedLength: header.paddedLength, contentMac: header.contentMac });
923
967
  }
924
968
  function isNonEmptyPlainObject(value) {
925
969
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
@@ -927,12 +971,12 @@ function isNonEmptyPlainObject(value) {
927
971
  return (prototype === Object.prototype || prototype === null) && Object.keys(value).length > 0;
928
972
  }
929
973
  function canonicalPlaintext(plaintext) {
930
- if (!isNonEmptyPlainObject(plaintext)) throw invalidInput("Sealed plaintext must be a non-empty plain JSON object");
931
974
  try {
932
- return canonicalizeJson(plaintext);
975
+ if (isNonEmptyPlainObject(plaintext)) return canonicalizeSealedJson(plaintext);
933
976
  } catch (cause) {
934
977
  throw new ByokCoreError("sealed_secret_invalid", "Sealed plaintext is not canonical JSON", { cause });
935
978
  }
979
+ throw invalidInput("Sealed plaintext must be a non-empty plain JSON object");
936
980
  }
937
981
  function parseCanonicalPlaintext(content) {
938
982
  let text;
@@ -940,7 +984,7 @@ function parseCanonicalPlaintext(content) {
940
984
  try {
941
985
  text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(content);
942
986
  value = JSON.parse(text);
943
- if (!isNonEmptyPlainObject(value) || canonicalizeJson(value) !== text) return void 0;
987
+ if (!isNonEmptyPlainObject(value) || canonicalizeSealedJson(value) !== text) return void 0;
944
988
  } catch {
945
989
  return void 0;
946
990
  }
@@ -979,7 +1023,7 @@ async function sealContentWithRandomness(input, randomness) {
979
1023
  if (content.byteLength > input.maxContentLength) throw invalidInput("Sealed content exceeds its plaintext limit");
980
1024
  const frame = encodeSealedFrame(content);
981
1025
  const { salt, nonce } = randomness;
982
- const parsed = SealedContentHeaderV1Schema.safeParse({
1026
+ const parsed = safeParseTotal(SealedContentHeaderV1Schema, {
983
1027
  v: SEALED_CONTENT_VERSION,
984
1028
  suite: SEALED_CONTENT_SUITE,
985
1029
  category: input.category,
@@ -1008,12 +1052,12 @@ function failure(reason) {
1008
1052
  }
1009
1053
  async function openSealedContent(input) {
1010
1054
  assertMaxContentLength(input.maxContentLength);
1011
- const parsedBinding = SealedBindingSchema.safeParse(input.binding);
1012
- const parsedSealing = SealedScopePolicySchema.safeParse(input.sealing);
1055
+ const parsedBinding = safeParseTotal(SealedBindingSchema, input.binding);
1056
+ const parsedSealing = safeParseTotal(SealedScopePolicySchema, input.sealing);
1013
1057
  if (!parsedBinding.success || !parsedSealing.success) return failure("sealing_policy_mismatch");
1014
1058
  const binding = parsedBinding.data;
1015
1059
  const sealing = parsedSealing.data;
1016
- const parsed = SealedContentHeaderV1Schema.safeParse(input.header);
1060
+ const parsed = safeParseTotal(SealedContentHeaderV1Schema, input.header);
1017
1061
  if (!parsed.success) return failure("sealed_open_failed");
1018
1062
  const header = parsed.data;
1019
1063
  if (!binding.categories.includes(header.category) || header.scope.id !== sealing.scopeId || header.scope.epoch < sealing.epoch) {
@@ -1042,7 +1086,401 @@ async function openSealedContent(input) {
1042
1086
  frame.fill(0);
1043
1087
  }
1044
1088
  }
1089
+ var MEMBER_KEY_BUNDLE_VERSION = 1;
1090
+ var MEMBER_KEM_DOMAIN_PREFIX = "byok-member-kem-v1\n";
1091
+ var KemEpochSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
1092
+ var KemSigSchema = base64UrlBytes(64);
1093
+ var MemberKeyBundleV1Schema = z.strictObject({
1094
+ v: z.literal(MEMBER_KEY_BUNDLE_VERSION),
1095
+ memberId: z.string().regex(MEMBER_ID_PATTERN),
1096
+ signingPublicJwk: P256PublicJwkSchema,
1097
+ kemPublicJwk: P256PublicJwkSchema,
1098
+ kemEpoch: KemEpochSchema,
1099
+ kemSig: KemSigSchema,
1100
+ deviceId: z.string().min(1).optional()
1101
+ });
1102
+ var CreateInputSchema = z.object({
1103
+ signingPublicJwk: P256PublicJwkSchema,
1104
+ signingPrivateKey: z.custom(),
1105
+ kemPublicJwk: P256PublicJwkSchema,
1106
+ kemEpoch: KemEpochSchema,
1107
+ deviceId: z.string().min(1).optional()
1108
+ });
1109
+ var KemSignerSchema = z.object({ memberId: z.string().regex(MEMBER_ID_PATTERN), signingPublicJwk: P256PublicJwkSchema });
1110
+ var KemStatementSchema = z.object({ kemPublicJwk: P256PublicJwkSchema, kemEpoch: KemEpochSchema, kemSig: KemSigSchema });
1111
+ function memberKemSigningInput(memberId, kemPublicJwk, kemEpoch) {
1112
+ return concat2(
1113
+ utf82(MEMBER_KEM_DOMAIN_PREFIX),
1114
+ canonicalizeJsonBytes({ memberId, kemPublicKey: { x: kemPublicJwk.x, y: kemPublicJwk.y }, kemEpoch })
1115
+ );
1116
+ }
1117
+ async function createMemberKeyBundle(input) {
1118
+ const parsed = safeParseTotal(CreateInputSchema, input);
1119
+ if (!parsed.success) throw invalidInput("Member key bundle input is invalid");
1120
+ const { signingPublicJwk, signingPrivateKey, kemPublicJwk, kemEpoch, deviceId } = parsed.data;
1121
+ const memberId = await deriveMemberId(signingPublicJwk);
1122
+ if (!await isValidMemberKemKey(kemPublicJwk, signingPublicJwk)) throw invalidInput("Member KEM key is invalid");
1123
+ const kemSig = await ecdsaSign(signingPublicJwk, signingPrivateKey, memberKemSigningInput(memberId, kemPublicJwk, kemEpoch));
1124
+ return {
1125
+ v: MEMBER_KEY_BUNDLE_VERSION,
1126
+ memberId,
1127
+ signingPublicJwk,
1128
+ kemPublicJwk,
1129
+ kemEpoch,
1130
+ kemSig,
1131
+ ...deviceId === void 0 ? {} : { deviceId }
1132
+ };
1133
+ }
1134
+ async function isValidMemberKemKey(kemPublicJwk, signingPublicJwk) {
1135
+ if (kemPublicJwk.x === signingPublicJwk.x && kemPublicJwk.y === signingPublicJwk.y) return false;
1136
+ try {
1137
+ await p256PublicKeyBytes(kemPublicJwk);
1138
+ return true;
1139
+ } catch {
1140
+ return false;
1141
+ }
1142
+ }
1143
+ async function verifyMemberKem(member, kem) {
1144
+ const signer = safeParseTotal(KemSignerSchema, member);
1145
+ const statement = safeParseTotal(KemStatementSchema, kem);
1146
+ if (!signer.success || !statement.success) return false;
1147
+ const { memberId, signingPublicJwk } = signer.data;
1148
+ const { kemPublicJwk, kemEpoch, kemSig } = statement.data;
1149
+ if (!await isValidMemberKemKey(kemPublicJwk, signingPublicJwk)) return false;
1150
+ try {
1151
+ return await ecdsaVerify(signingPublicJwk, kemSig, memberKemSigningInput(memberId, kemPublicJwk, kemEpoch));
1152
+ } catch {
1153
+ return false;
1154
+ }
1155
+ }
1156
+ async function parseMemberKeyBundle(value) {
1157
+ const parsed = safeParseTotal(MemberKeyBundleV1Schema, value);
1158
+ if (!parsed.success) return void 0;
1159
+ const bundle = parsed.data;
1160
+ try {
1161
+ if (await deriveMemberId(bundle.signingPublicJwk) !== bundle.memberId) return void 0;
1162
+ } catch {
1163
+ return void 0;
1164
+ }
1165
+ if (!await verifyMemberKem(bundle, bundle)) return void 0;
1166
+ return bundle;
1167
+ }
1168
+ var MEMBER_FINGERPRINT_DOMAIN = "byok-member-fingerprint-v1\n";
1169
+ var FINGERPRINT_GROUPS = 5;
1170
+ var GROUP_BYTES = 5;
1171
+ var GROUP_MODULUS = 100000n;
1172
+ var FingerprintInputSchema = z.object({ tenantId: z.string().min(1), signingPublicJwk: P256PublicJwkSchema });
1173
+ async function memberFingerprint(input) {
1174
+ return (await fingerprintGroups(input)).groups.join(" ");
1175
+ }
1176
+ async function memberApprovalCode(tenantId, first, second) {
1177
+ const members = await Promise.all([first, second].map((signingPublicJwk) => fingerprintGroups({ tenantId, signingPublicJwk })));
1178
+ if (members[0].memberId === members[1].memberId) throw invalidInput("An approval code needs two different members");
1179
+ members.sort((a, b) => a.memberId < b.memberId ? -1 : 1);
1180
+ return members.flatMap((member) => member.groups).join(" ");
1181
+ }
1182
+ async function fingerprintGroups(input) {
1183
+ const parsed = safeParseTotal(FingerprintInputSchema, input);
1184
+ if (!parsed.success) throw invalidInput("Member fingerprint needs a tenant id and a P-256 signing key");
1185
+ const { tenantId, signingPublicJwk } = parsed.data;
1186
+ const memberId = await deriveMemberId(signingPublicJwk);
1187
+ const digest = await sha256(
1188
+ concat2(
1189
+ utf82(MEMBER_FINGERPRINT_DOMAIN),
1190
+ canonicalizeJsonBytes({ v: 1, tenantId, signingKey: { x: signingPublicJwk.x, y: signingPublicJwk.y } })
1191
+ )
1192
+ );
1193
+ const groups = [];
1194
+ for (let group = 0; group < FINGERPRINT_GROUPS; group += 1) {
1195
+ let value = 0n;
1196
+ for (const byte of digest.subarray(group * GROUP_BYTES, (group + 1) * GROUP_BYTES)) value = value << 8n | BigInt(byte);
1197
+ groups.push((value % GROUP_MODULUS).toString().padStart(5, "0"));
1198
+ }
1199
+ return { memberId, groups };
1200
+ }
1201
+ var PAIRING_VERIFICATION_SECRET_BYTES = 16;
1202
+ var PAIRING_VERIFICATION_TEXT_LENGTH = 26;
1203
+ var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1204
+ var PV_TEXT_PATTERN = /^[A-Za-z2-7]{26}$/u;
1205
+ function generatePairingVerificationSecret() {
1206
+ return randomBytes(PAIRING_VERIFICATION_SECRET_BYTES);
1207
+ }
1208
+ function encodePairingVerificationSecret(pv) {
1209
+ if (pv.byteLength !== PAIRING_VERIFICATION_SECRET_BYTES) {
1210
+ throw invalidInput(`Pairing verification secret must be ${PAIRING_VERIFICATION_SECRET_BYTES} bytes`);
1211
+ }
1212
+ let out = "";
1213
+ let buffer = 0;
1214
+ let bits = 0;
1215
+ for (const byte of pv) {
1216
+ buffer = buffer << 8 | byte;
1217
+ bits += 8;
1218
+ while (bits >= 5) {
1219
+ bits -= 5;
1220
+ out += BASE32_ALPHABET[buffer >>> bits & 31];
1221
+ }
1222
+ buffer &= (1 << bits) - 1;
1223
+ }
1224
+ return out + BASE32_ALPHABET[buffer << 5 - bits & 31];
1225
+ }
1226
+ function decodePairingVerificationSecret(text) {
1227
+ if (!PV_TEXT_PATTERN.test(text)) {
1228
+ throw invalidInput(`Pairing verification secret must be ${PAIRING_VERIFICATION_TEXT_LENGTH} base32 characters`);
1229
+ }
1230
+ const upper = text.toUpperCase();
1231
+ const out = new Uint8Array(PAIRING_VERIFICATION_SECRET_BYTES);
1232
+ let buffer = 0;
1233
+ let bits = 0;
1234
+ let index = 0;
1235
+ for (const char of upper) {
1236
+ buffer = buffer << 5 | BASE32_ALPHABET.indexOf(char);
1237
+ bits += 5;
1238
+ if (bits >= 8) {
1239
+ bits -= 8;
1240
+ out[index++] = buffer >>> bits & 255;
1241
+ }
1242
+ buffer &= (1 << bits) - 1;
1243
+ }
1244
+ if (buffer !== 0) throw invalidInput("Pairing verification secret has nonzero trailing bits");
1245
+ return out;
1246
+ }
1247
+ function formatPairingInput(pairingCode, pv) {
1248
+ if (pairingCode.length === 0) throw invalidInput("Pairing code must not be empty");
1249
+ return `${pairingCode}.${encodePairingVerificationSecret(pv)}`;
1250
+ }
1251
+ function parsePairingInput(text) {
1252
+ const dot = text.lastIndexOf(".");
1253
+ if (dot <= 0) throw invalidInput("Pairing input must be <pairing code>.<verification secret>");
1254
+ return { pairingCode: text.slice(0, dot), pv: decodePairingVerificationSecret(text.slice(dot + 1)) };
1255
+ }
1256
+ var DEVICE_PAIRING_MAC_DOMAIN = "byok-pairing-device-mac-v1\n";
1257
+ var CLIENT_PAIRING_MAC_DOMAIN = "byok-pairing-client-mac-v1\n";
1258
+ var PairingMacSchema = base64UrlBytes(32);
1259
+ var ChainHeadSchema = z.strictObject({
1260
+ seq: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
1261
+ hash: z.string().regex(CONTENT_HASH_PATTERN)
1262
+ });
1263
+ var DeviceMacInputSchema = z.object({ tenantId: z.string().min(1), deviceId: z.string().min(1), member: MemberKeyBundleV1Schema });
1264
+ var ClientMacInputSchema = z.object({ member: MemberKeyBundleV1Schema, chainHead: ChainHeadSchema });
1265
+ function devicePairingMacInput(input) {
1266
+ const parsed = safeParseTotal(DeviceMacInputSchema, input);
1267
+ if (!parsed.success || parsed.data.member.deviceId !== parsed.data.deviceId) {
1268
+ throw invalidInput("Device pairing MAC input must name the device member's own device id");
1269
+ }
1270
+ const { tenantId, deviceId, member } = parsed.data;
1271
+ return concat2(utf82(DEVICE_PAIRING_MAC_DOMAIN), canonicalizeJsonBytes({ tenantId, deviceId, member }));
1272
+ }
1273
+ function clientPairingMacInput(input) {
1274
+ const parsed = safeParseTotal(ClientMacInputSchema, input);
1275
+ if (!parsed.success || parsed.data.member.deviceId !== void 0) {
1276
+ throw invalidInput("Client pairing MAC input must hold a client member and a chain head");
1277
+ }
1278
+ const { member, chainHead } = parsed.data;
1279
+ return concat2(utf82(CLIENT_PAIRING_MAC_DOMAIN), canonicalizeJsonBytes({ member, chainHead: { seq: chainHead.seq, hash: chainHead.hash } }));
1280
+ }
1281
+ function checkPv(pv) {
1282
+ if (pv.byteLength !== PAIRING_VERIFICATION_SECRET_BYTES) {
1283
+ throw invalidInput(`Pairing verification secret must be ${PAIRING_VERIFICATION_SECRET_BYTES} bytes`);
1284
+ }
1285
+ }
1286
+ async function computeDevicePairingMac(pv, input) {
1287
+ checkPv(pv);
1288
+ return encodeBase64Url(await hmacSha2562(pv, devicePairingMacInput(input)));
1289
+ }
1290
+ async function computeClientPairingMac(pv, input) {
1291
+ checkPv(pv);
1292
+ return encodeBase64Url(await hmacSha2562(pv, clientPairingMacInput(input)));
1293
+ }
1294
+ async function verifyDevicePairingMac(pv, input, mac) {
1295
+ checkPv(pv);
1296
+ return hmacVerify(pv, () => devicePairingMacInput(input), mac);
1297
+ }
1298
+ async function verifyClientPairingMac(pv, input, mac) {
1299
+ checkPv(pv);
1300
+ return hmacVerify(pv, () => clientPairingMacInput(input), mac);
1301
+ }
1302
+ async function hmacVerify(key, buildData, mac) {
1303
+ const parsed = safeParseTotal(PairingMacSchema, mac);
1304
+ if (!parsed.success) return false;
1305
+ let data;
1306
+ try {
1307
+ data = buildData();
1308
+ } catch {
1309
+ return false;
1310
+ }
1311
+ const subtle = webCryptoSubtle();
1312
+ const hmacKey = await subtle.importKey("raw", copy2(key), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
1313
+ return subtle.verify("HMAC", hmacKey, decodeBase64Url(parsed.data, "mac"), data);
1314
+ }
1315
+ var MemberIdSchema = z.string().regex(MEMBER_ID_PATTERN);
1316
+ var ScopeIdSchema = z.string().min(1);
1317
+ var AddSubjectSchema = z.strictObject({ member: z.unknown() });
1318
+ var RemoveSubjectSchema = z.strictObject({ memberId: MemberIdSchema });
1319
+ var RotateSubjectSchema = z.strictObject({
1320
+ memberId: MemberIdSchema,
1321
+ kemPublicJwk: P256PublicJwkSchema,
1322
+ kemEpoch: KemEpochSchema,
1323
+ kemSig: KemSigSchema
1324
+ });
1325
+ var ScopePolicySubjectSchema = z.strictObject({
1326
+ scopeId: ScopeIdSchema,
1327
+ categories: z.array(z.enum(SEALED_CATEGORIES)).min(1),
1328
+ members: z.array(MemberIdSchema).min(1)
1329
+ });
1330
+ var ShredSubjectSchema = z.strictObject({ scopeId: ScopeIdSchema });
1331
+ var SignerSchema = z.object({ signer: MemberIdSchema });
1332
+ var GenesisSchema = z.object({ op: z.literal("add"), subject: AddSubjectSchema });
1333
+ var HeadSchema = z.object({ seq: z.number(), hash: z.string() });
1334
+ var EntriesSchema = z.array(z.unknown());
1335
+ var AnchorInputSchema = z.object({
1336
+ tenantId: z.string(),
1337
+ entries: EntriesSchema,
1338
+ self: z.unknown(),
1339
+ approverSigningPublicJwk: P256PublicJwkSchema,
1340
+ authenticatedHead: z.unknown().optional()
1341
+ });
1342
+ function emptyMemberChainState(tenantId) {
1343
+ return { tenantId, head: null, members: /* @__PURE__ */ new Map(), removed: /* @__PURE__ */ new Set(), scopes: /* @__PURE__ */ new Map(), shredded: /* @__PURE__ */ new Set() };
1344
+ }
1345
+ function memberChainScopeRecipients(state, scopeId) {
1346
+ const scope = state.scopes.get(scopeId);
1347
+ if (scope === void 0) return [];
1348
+ return scope.members.flatMap((memberId) => {
1349
+ const member = state.members.get(memberId);
1350
+ return member === void 0 ? [] : [member];
1351
+ });
1352
+ }
1353
+ async function applyMemberChainEntries(state, entries) {
1354
+ const list = safeParseTotal(EntriesSchema, entries);
1355
+ if (!list.success) return { ok: false, reason: "member_chain_entry_invalid", index: 0 };
1356
+ const result = await walk(state, list.data, () => false);
1357
+ return result.ok ? { ok: true, state: result.state } : result;
1358
+ }
1359
+ async function genesisSigner(state, raw, signer) {
1360
+ if (state.head !== null || state.members.size > 0) return void 0;
1361
+ const entry = safeParseTotal(GenesisSchema, raw);
1362
+ const member = entry.success ? await parseMemberKeyBundle(entry.data.subject.member) : void 0;
1363
+ return member !== void 0 && member.memberId === signer ? member : void 0;
1364
+ }
1365
+ async function walk(state, entries, mark) {
1366
+ const next = {
1367
+ tenantId: state.tenantId,
1368
+ head: state.head,
1369
+ members: new Map(state.members),
1370
+ removed: new Set(state.removed),
1371
+ scopes: new Map(state.scopes),
1372
+ shredded: new Set(state.shredded)
1373
+ };
1374
+ let marked;
1375
+ for (const [index, raw] of entries.entries()) {
1376
+ const signer = safeParseTotal(SignerSchema, raw);
1377
+ if (!signer.success) return { ok: false, reason: "member_chain_entry_invalid", index };
1378
+ const signerBundle = next.members.get(signer.data.signer) ?? await genesisSigner(next, raw, signer.data.signer);
1379
+ if (signerBundle === void 0) return { ok: false, reason: "member_chain_signer_invalid", index };
1380
+ const verified = await verifyMemberChainEntry(raw, signerBundle.signingPublicJwk);
1381
+ if (!verified.ok) return { ok: false, reason: verified.reason, index };
1382
+ const entry = verified.entry;
1383
+ const expected = next.head === null ? { seq: 1, prev: null } : { seq: next.head.seq + 1, prev: next.head.hash };
1384
+ if (entry.tenantId !== next.tenantId || entry.seq !== expected.seq || entry.prev !== expected.prev) {
1385
+ return { ok: false, reason: "member_chain_link_invalid", index };
1386
+ }
1387
+ if (marked === void 0 && mark(entry, signerBundle.signingPublicJwk)) marked = { index, seq: entry.seq, hash: verified.hash };
1388
+ const applied = await applySubject(next, entry);
1389
+ if (applied !== true) return { ok: false, reason: applied === false ? "member_chain_subject_invalid" : applied, index };
1390
+ next.head = { seq: entry.seq, hash: verified.hash };
1391
+ }
1392
+ return { ok: true, state: next, marked };
1393
+ }
1394
+ async function applySubject(state, entry) {
1395
+ switch (entry.op) {
1396
+ case "add": {
1397
+ const subject = safeParseTotal(AddSubjectSchema, entry.subject);
1398
+ const member = subject.success ? await parseMemberKeyBundle(subject.data.member) : void 0;
1399
+ if (member === void 0 || state.members.has(member.memberId) || state.removed.has(member.memberId)) return false;
1400
+ state.members.set(member.memberId, member);
1401
+ return true;
1402
+ }
1403
+ case "remove": {
1404
+ const subject = safeParseTotal(RemoveSubjectSchema, entry.subject);
1405
+ if (!subject.success || !state.members.has(subject.data.memberId) || state.members.size === 1) return false;
1406
+ state.members.delete(subject.data.memberId);
1407
+ state.removed.add(subject.data.memberId);
1408
+ return true;
1409
+ }
1410
+ case "rotate": {
1411
+ const subject = safeParseTotal(RotateSubjectSchema, entry.subject);
1412
+ if (!subject.success || subject.data.memberId !== entry.signer) return false;
1413
+ const member = state.members.get(subject.data.memberId);
1414
+ if (member === void 0 || subject.data.kemEpoch !== member.kemEpoch + 1) return false;
1415
+ const { kemPublicJwk } = subject.data;
1416
+ if (kemPublicJwk.x === member.kemPublicJwk.x && kemPublicJwk.y === member.kemPublicJwk.y) return false;
1417
+ if (!await verifyMemberKem(member, subject.data)) return false;
1418
+ state.members.set(member.memberId, {
1419
+ ...member,
1420
+ kemPublicJwk: subject.data.kemPublicJwk,
1421
+ kemEpoch: subject.data.kemEpoch,
1422
+ kemSig: subject.data.kemSig
1423
+ });
1424
+ return true;
1425
+ }
1426
+ case "scope-policy": {
1427
+ const subject = safeParseTotal(ScopePolicySubjectSchema, entry.subject);
1428
+ if (!subject.success || state.shredded.has(subject.data.scopeId)) return false;
1429
+ const { categories, members } = subject.data;
1430
+ if (!strictlyIncreasing(categories.map((category) => SEALED_CATEGORIES.indexOf(category)))) return false;
1431
+ if (!strictlyIncreasing(members) || !members.every((memberId) => state.members.has(memberId))) return false;
1432
+ if (!(state.scopes.get(subject.data.scopeId)?.members ?? members).includes(entry.signer)) return "member_chain_signer_invalid";
1433
+ state.scopes.set(subject.data.scopeId, { categories, members, policySeq: entry.seq });
1434
+ return true;
1435
+ }
1436
+ case "shred": {
1437
+ const subject = safeParseTotal(ShredSubjectSchema, entry.subject);
1438
+ if (!subject.success) return false;
1439
+ const scope = state.scopes.get(subject.data.scopeId);
1440
+ if (scope === void 0) return false;
1441
+ if (!scope.members.includes(entry.signer)) return "member_chain_signer_invalid";
1442
+ state.scopes.delete(subject.data.scopeId);
1443
+ state.shredded.add(subject.data.scopeId);
1444
+ return true;
1445
+ }
1446
+ }
1447
+ }
1448
+ function strictlyIncreasing(values) {
1449
+ return values.every((value, index) => index === 0 || values[index - 1] < value);
1450
+ }
1451
+ async function verifyMemberChainFromAnchor(input) {
1452
+ const parsed = safeParseTotal(AnchorInputSchema, input);
1453
+ if (!parsed.success) return { ok: false, reason: "member_chain_anchor_missing" };
1454
+ const { tenantId, entries, self, approverSigningPublicJwk: approver, authenticatedHead } = parsed.data;
1455
+ let approverId;
1456
+ let selfText;
1457
+ try {
1458
+ approverId = await deriveMemberId(approver);
1459
+ selfText = canonicalizeJson(self);
1460
+ } catch {
1461
+ return { ok: false, reason: "member_chain_anchor_missing" };
1462
+ }
1463
+ const isAnchor = (entry, signerKey) => {
1464
+ if (entry.op !== "add" || entry.signer !== approverId || signerKey.x !== approver.x || signerKey.y !== approver.y) return false;
1465
+ try {
1466
+ return canonicalizeJson(entry.subject.member) === selfText;
1467
+ } catch {
1468
+ return false;
1469
+ }
1470
+ };
1471
+ const outcome = await walk(emptyMemberChainState(tenantId), entries, isAnchor);
1472
+ if (!outcome.ok) return outcome;
1473
+ const anchor = outcome.marked;
1474
+ if (anchor === void 0) return { ok: false, reason: "member_chain_anchor_missing" };
1475
+ if (authenticatedHead !== void 0) {
1476
+ const head = safeParseTotal(HeadSchema, authenticatedHead);
1477
+ if (!head.success || head.data.seq !== anchor.seq || head.data.hash !== anchor.hash) {
1478
+ return { ok: false, reason: "member_chain_head_mismatch", index: anchor.index };
1479
+ }
1480
+ }
1481
+ return { ok: true, state: outcome.state, anchorSeq: anchor.seq };
1482
+ }
1045
1483
 
1046
- export { MEMBER_CHAIN_OPS, SEALED_CATEGORIES, SEALED_CONTENT_SUITE, SEALED_CONTENT_VERSION, SEALED_DIRECTIONS, SealedContentHeaderV1Schema, deriveMemberId, deriveScopeKeyId, generateScopeKey, memberChainEntryHash, openScopeKeyPackage, openSealedContent, sealContent, sealScopeKeyPackage, signMemberChainEntry, verifyMemberChainEntry };
1484
+ export { CLIENT_PAIRING_MAC_DOMAIN, DEVICE_PAIRING_MAC_DOMAIN, MEMBER_CHAIN_OPS, MEMBER_FINGERPRINT_DOMAIN, MEMBER_KEY_BUNDLE_VERSION, PAIRING_VERIFICATION_SECRET_BYTES, SEALED_CATEGORIES, SEALED_CONTENT_SUITE, SEALED_CONTENT_VERSION, SEALED_DIRECTIONS, SealedContentHeaderV1Schema, applyMemberChainEntries, computeClientPairingMac, computeDevicePairingMac, createMemberKeyBundle, decodePairingVerificationSecret, deriveMemberId, deriveScopeKeyId, emptyMemberChainState, encodePairingVerificationSecret, formatPairingInput, generatePairingVerificationSecret, generateScopeKey, memberApprovalCode, memberChainEntryHash, memberChainScopeRecipients, memberFingerprint, openScopeKeyPackage, openSealedContent, parseMemberKeyBundle, parsePairingInput, sealContent, sealScopeKeyPackage, signMemberChainEntry, verifyClientPairingMac, verifyDevicePairingMac, verifyMemberChainEntry, verifyMemberChainFromAnchor, verifyMemberKem };
1047
1485
  //# sourceMappingURL=index.js.map
1048
1486
  //# sourceMappingURL=index.js.map