@tokenoftrust/storefront-runner 4.10.1 → 4.11.1
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/agency-kit/tools/lib/analytics-declaration.mjs +21 -5
- package/apps/storefront/migrations-apps/0021_contact_vault.sql +4 -0
- package/apps/storefront/migrations-apps/0022_contact_vault_fields.sql +342 -0
- package/apps/storefront/src/lib/customers/contactVault/blindIndex.ts +28 -0
- package/apps/storefront/src/lib/customers/contactVault/contactAudit.ts +21 -7
- package/apps/storefront/src/lib/customers/contactVault/dataKeys.ts +256 -0
- package/apps/storefront/src/lib/customers/contactVault/fieldEnvelope.ts +88 -0
- package/apps/storefront/src/lib/customers/contactVault/isolateMetrics.ts +37 -0
- package/apps/storefront/src/lib/customers/contactVault/kms.ts +292 -0
- package/apps/storefront/src/lib/customers/contactVault/retention.ts +270 -12
- package/apps/storefront/src/lib/customers/contactVault/retentionPolicy.ts +290 -0
- package/apps/storefront/src/lib/customers/contactVault/zip3.ts +24 -0
- package/apps/storefront/src/lib/engagement/contact/contactMessages.ts +458 -118
- package/apps/storefront/src/lib/engagement/contact/messagesTabClient.ts +33 -3
- package/apps/storefront/src/lib/integrations/integration/stageDestinationIds.ts +5 -2
- package/apps/storefront/src/lib/orders/admin/orderActions.ts +180 -32
- package/apps/storefront/src/lib/orders/vault/contactIntake.ts +5 -3
- package/apps/storefront/src/lib/orders/vault/contactRoutes.ts +4 -4
- package/apps/storefront/src/lib/orders/vault/kmsEnv.ts +11 -0
- package/apps/storefront/src/lib/orders/vault/orderContactVault.ts +1053 -227
- package/apps/storefront/src/lib/orders/vault/vault-test-support.ts +258 -0
- package/apps/storefront/src/pages/api/admin/fulfillment/orders/[id]/address.ts +34 -5
- package/apps/storefront/src/pages/api/admin/fulfillment/orders/contact-search.ts +25 -0
- package/apps/storefront/src/pages/api/admin/fulfillment/orders/contacts.ts +21 -0
- package/apps/storefront/src/pages/api/contact.ts +7 -9
- package/apps/storefront/src/scheduled/contactVaultSweep.ts +55 -26
- package/package.json +1 -1
- package/packages/public-runtime/src/integration/platform-adapters.ts +4 -2
- package/packages/public-runtime/src/integration/result.ts +12 -2
- package/apps/storefront/src/lib/customers/contactVault/envelope.ts +0 -166
- package/apps/storefront/src/lib/orders/vault/keyringEnv.ts +0 -7
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contact vault envelope: AES-256-GCM under a per-tenant, per-purpose HKDF subkey of one keyring
|
|
3
|
-
* key. The AAD binds tenant, stage, record id, key id and the immutable `notAfter`, so a row read
|
|
4
|
-
* under any other binding fails its tag and reads as absent.
|
|
5
|
-
*
|
|
6
|
-
* The keyring is the `ORDER_CONTACT_VAULT_KEYRING` Worker secret: JSON
|
|
7
|
-
* `{ active: <kid>, keys: { <kid>: <base64 of 32 bytes> } }`. Errors carry fixed codes only —
|
|
8
|
-
* never a key, nonce, ciphertext or plaintext value.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export type VaultPurpose = "order-contact" | "contact-message";
|
|
12
|
-
|
|
13
|
-
export interface VaultKeyring {
|
|
14
|
-
active: string;
|
|
15
|
-
keys: ReadonlyMap<string, Uint8Array>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface VaultBinding {
|
|
19
|
-
purpose: VaultPurpose;
|
|
20
|
-
tenantId: string;
|
|
21
|
-
stage: string;
|
|
22
|
-
recordId: string;
|
|
23
|
-
notAfter: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SealedEnvelope {
|
|
27
|
-
keyId: string;
|
|
28
|
-
nonce: string;
|
|
29
|
-
ciphertext: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class VaultKeyUnavailableError extends Error {
|
|
33
|
-
readonly code = "vault-key-unavailable";
|
|
34
|
-
constructor() {
|
|
35
|
-
super("vault-key-unavailable");
|
|
36
|
-
this.name = "VaultKeyUnavailableError";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const KID = /^[A-Za-z0-9._-]{1,32}$/;
|
|
41
|
-
const HKDF_SALT = new TextEncoder().encode("tot-contact-vault/v1");
|
|
42
|
-
const encoder = new TextEncoder();
|
|
43
|
-
const decoder = new TextDecoder();
|
|
44
|
-
|
|
45
|
-
function fromBase64(value: string): Uint8Array | null {
|
|
46
|
-
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
|
|
47
|
-
let binary: string;
|
|
48
|
-
try {
|
|
49
|
-
binary = atob(value);
|
|
50
|
-
} catch (error) {
|
|
51
|
-
if (error instanceof DOMException && error.name === "InvalidCharacterError") return null;
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
const out = new Uint8Array(binary.length);
|
|
55
|
-
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function toBase64(bytes: Uint8Array): string {
|
|
60
|
-
let binary = "";
|
|
61
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
62
|
-
return btoa(binary);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Parse the keyring secret, or null when it is absent or malformed in any way. */
|
|
66
|
-
export function parseVaultKeyring(raw: string | undefined | null): VaultKeyring | null {
|
|
67
|
-
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
68
|
-
let parsed: unknown;
|
|
69
|
-
try {
|
|
70
|
-
parsed = JSON.parse(raw);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
if (error instanceof SyntaxError) return null;
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
76
|
-
const { active, keys } = parsed as { active?: unknown; keys?: unknown };
|
|
77
|
-
if (typeof active !== "string" || !KID.test(active)) return null;
|
|
78
|
-
if (!keys || typeof keys !== "object" || Array.isArray(keys)) return null;
|
|
79
|
-
const map = new Map<string, Uint8Array>();
|
|
80
|
-
for (const [kid, value] of Object.entries(keys as Record<string, unknown>)) {
|
|
81
|
-
if (!KID.test(kid) || typeof value !== "string") return null;
|
|
82
|
-
const bytes = fromBase64(value);
|
|
83
|
-
if (!bytes || bytes.length !== 32) return null;
|
|
84
|
-
map.set(kid, bytes);
|
|
85
|
-
}
|
|
86
|
-
if (!map.has(active)) return null;
|
|
87
|
-
return { active, keys: map };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Canonical AAD bytes: fixed key order, so equal bindings always produce equal AAD. */
|
|
91
|
-
export function vaultAad(binding: VaultBinding, keyId: string): Uint8Array {
|
|
92
|
-
return encoder.encode(JSON.stringify({
|
|
93
|
-
v: 1,
|
|
94
|
-
purpose: binding.purpose,
|
|
95
|
-
tenantId: binding.tenantId,
|
|
96
|
-
stage: binding.stage,
|
|
97
|
-
recordId: binding.recordId,
|
|
98
|
-
keyId,
|
|
99
|
-
notAfter: binding.notAfter,
|
|
100
|
-
}));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function subkey(keyring: VaultKeyring, keyId: string, binding: VaultBinding): Promise<CryptoKey> {
|
|
104
|
-
const ikm = keyring.keys.get(keyId);
|
|
105
|
-
if (!ikm) throw new VaultKeyUnavailableError();
|
|
106
|
-
const base = await crypto.subtle.importKey("raw", ikm as BufferSource, "HKDF", false, ["deriveKey"]);
|
|
107
|
-
return crypto.subtle.deriveKey(
|
|
108
|
-
{ name: "HKDF", hash: "SHA-256", salt: HKDF_SALT, info: encoder.encode(`${binding.purpose}|${binding.tenantId}`) },
|
|
109
|
-
base,
|
|
110
|
-
{ name: "AES-GCM", length: 256 },
|
|
111
|
-
false,
|
|
112
|
-
["encrypt", "decrypt"],
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Seal a JSON value under the keyring's active key. */
|
|
117
|
-
export async function sealVaultRecord(
|
|
118
|
-
keyring: VaultKeyring | null,
|
|
119
|
-
binding: VaultBinding,
|
|
120
|
-
value: unknown,
|
|
121
|
-
): Promise<SealedEnvelope> {
|
|
122
|
-
if (!keyring) throw new VaultKeyUnavailableError();
|
|
123
|
-
const keyId = keyring.active;
|
|
124
|
-
const key = await subkey(keyring, keyId, binding);
|
|
125
|
-
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
126
|
-
const sealed = await crypto.subtle.encrypt(
|
|
127
|
-
{ name: "AES-GCM", iv: nonce, additionalData: vaultAad(binding, keyId) as BufferSource },
|
|
128
|
-
key,
|
|
129
|
-
encoder.encode(JSON.stringify(value)),
|
|
130
|
-
);
|
|
131
|
-
return { keyId, nonce: toBase64(nonce), ciphertext: toBase64(new Uint8Array(sealed)) };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Open a sealed record. Null when the binding, nonce or ciphertext does not authenticate — callers
|
|
136
|
-
* report that as not-found. A missing keyring or key id throws {@link VaultKeyUnavailableError}.
|
|
137
|
-
*/
|
|
138
|
-
export async function openVaultRecord(
|
|
139
|
-
keyring: VaultKeyring | null,
|
|
140
|
-
binding: VaultBinding,
|
|
141
|
-
envelope: SealedEnvelope,
|
|
142
|
-
): Promise<unknown | null> {
|
|
143
|
-
if (!keyring) throw new VaultKeyUnavailableError();
|
|
144
|
-
const key = await subkey(keyring, envelope.keyId, binding);
|
|
145
|
-
const nonce = fromBase64(envelope.nonce);
|
|
146
|
-
const ciphertext = fromBase64(envelope.ciphertext);
|
|
147
|
-
if (!nonce || nonce.length !== 12 || !ciphertext) return null;
|
|
148
|
-
let plain: ArrayBuffer;
|
|
149
|
-
try {
|
|
150
|
-
plain = await crypto.subtle.decrypt(
|
|
151
|
-
{ name: "AES-GCM", iv: nonce as BufferSource, additionalData: vaultAad(binding, envelope.keyId) as BufferSource },
|
|
152
|
-
key,
|
|
153
|
-
ciphertext as BufferSource,
|
|
154
|
-
);
|
|
155
|
-
} catch (error) {
|
|
156
|
-
// AES-GCM reports a failed tag (wrong binding, key or bytes) as OperationError; anything else is a fault.
|
|
157
|
-
if (error instanceof DOMException && error.name === "OperationError") return null;
|
|
158
|
-
throw error;
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
return JSON.parse(decoder.decode(plain));
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (error instanceof SyntaxError) return null;
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/** The Worker's contact vault keyring, from the declared `ORDER_CONTACT_VAULT_KEYRING` secret only. */
|
|
2
|
-
import { readEnv } from "@/lib/publication/shared/env";
|
|
3
|
-
import { parseVaultKeyring, type VaultKeyring } from "@/lib/customers/contactVault/envelope.js";
|
|
4
|
-
|
|
5
|
-
export async function loadVaultKeyring(): Promise<VaultKeyring | null> {
|
|
6
|
-
return parseVaultKeyring(await readEnv("ORDER_CONTACT_VAULT_KEYRING"));
|
|
7
|
-
}
|