@tokenoftrust/storefront-runner 4.11.0 → 4.11.2
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/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/package.json +1 -1
- 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/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/package.json +1 -1
- package/apps/storefront/src/lib/customers/contactVault/envelope.ts +0 -166
- package/apps/storefront/src/lib/orders/vault/keyringEnv.ts +0 -7
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
--
|
|
4
4
|
-- The plaintext scrappy_order_addresses table is dropped only when it is empty: the guard row's
|
|
5
5
|
-- CHECK fails on any other count and aborts the whole file.
|
|
6
|
+
--
|
|
7
|
+
-- allow-destructive: the three drops lose no data — _gv_vault_guard is this file's own guard,
|
|
8
|
+
-- scrappy_order_addresses is dropped only after the guard proves it empty, and contact_messages is
|
|
9
|
+
-- dropped only after every row is copied into contact_messages_sealed (migration0021.test.ts).
|
|
6
10
|
CREATE TABLE `_gv_vault_guard` (`n` integer NOT NULL CHECK (`n` = 0));
|
|
7
11
|
--> statement-breakpoint
|
|
8
12
|
INSERT INTO `_gv_vault_guard` (`n`) SELECT COUNT(*) FROM `scrappy_order_addresses`;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
-- Field-level contact vault: each personal field is its own AES-256-GCM ciphertext under a
|
|
2
|
+
-- per-tenant data key wrapped by AWS KMS (src/lib/customers/contactVault/). Retention is per
|
|
3
|
+
-- field group: an immutable ceiling bound into every field's AAD, a shrink-only effective expiry,
|
|
4
|
+
-- and an immutable seal-time floor (retentionPolicy.ts). The vault row is also the long-lived
|
|
5
|
+
-- facts row: after its last group is cleared it is detached from the order and keeps only
|
|
6
|
+
-- unlinked month, zip3, region, country and totals.
|
|
7
|
+
--
|
|
8
|
+
-- allow-destructive: order_contact_vault, order_contact_tombstones and contact_access_audit are
|
|
9
|
+
-- dropped and rebuilt only when empty (the guard below aborts the whole file otherwise), and
|
|
10
|
+
-- contact_messages is rebuilt preserving every row; no contact data exists in any of them yet.
|
|
11
|
+
CREATE TABLE `_gv_vault_v2_guard` (`n` integer NOT NULL CHECK (`n` = 0));
|
|
12
|
+
--> statement-breakpoint
|
|
13
|
+
INSERT INTO `_gv_vault_v2_guard` (`n`)
|
|
14
|
+
SELECT (SELECT COUNT(*) FROM `order_contact_vault`)
|
|
15
|
+
+ (SELECT COUNT(*) FROM `order_contact_tombstones`)
|
|
16
|
+
+ (SELECT COUNT(*) FROM `contact_access_audit`)
|
|
17
|
+
+ (SELECT COUNT(*) FROM `contact_messages` WHERE `ciphertext` IS NOT NULL);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
DROP TABLE `_gv_vault_v2_guard`;
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
DROP TABLE `order_contact_vault`;
|
|
22
|
+
--> statement-breakpoint
|
|
23
|
+
DROP TABLE `order_contact_tombstones`;
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
DROP TABLE `contact_access_audit`;
|
|
26
|
+
--> statement-breakpoint
|
|
27
|
+
CREATE TABLE `contact_vault_data_keys` (
|
|
28
|
+
`tenant_id` text NOT NULL,
|
|
29
|
+
`stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
|
|
30
|
+
`purpose` text NOT NULL CHECK (`purpose` IN ('order-contact', 'contact-message', 'blind-index')),
|
|
31
|
+
`kid` text NOT NULL,
|
|
32
|
+
`wrapped_key` text NOT NULL,
|
|
33
|
+
`kms_key_arn` text NOT NULL,
|
|
34
|
+
`created_at` text NOT NULL,
|
|
35
|
+
`retired_at` text,
|
|
36
|
+
PRIMARY KEY (`tenant_id`, `stage`, `purpose`, `kid`)
|
|
37
|
+
);
|
|
38
|
+
--> statement-breakpoint
|
|
39
|
+
CREATE UNIQUE INDEX `uq_contact_vault_data_keys_active`
|
|
40
|
+
ON `contact_vault_data_keys` (`tenant_id`, `stage`, `purpose`) WHERE `retired_at` IS NULL;
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
CREATE TRIGGER `contact_vault_data_keys_immutable`
|
|
43
|
+
BEFORE UPDATE ON `contact_vault_data_keys`
|
|
44
|
+
WHEN NEW.`wrapped_key` IS NOT OLD.`wrapped_key`
|
|
45
|
+
OR NEW.`kms_key_arn` IS NOT OLD.`kms_key_arn`
|
|
46
|
+
OR NEW.`kid` IS NOT OLD.`kid`
|
|
47
|
+
OR NEW.`tenant_id` IS NOT OLD.`tenant_id`
|
|
48
|
+
OR NEW.`stage` IS NOT OLD.`stage`
|
|
49
|
+
OR NEW.`purpose` IS NOT OLD.`purpose`
|
|
50
|
+
OR (OLD.`retired_at` IS NOT NULL AND NEW.`retired_at` IS NOT OLD.`retired_at`)
|
|
51
|
+
BEGIN
|
|
52
|
+
SELECT RAISE(ABORT, 'data-key-immutable');
|
|
53
|
+
END;
|
|
54
|
+
--> statement-breakpoint
|
|
55
|
+
CREATE TABLE `order_contact_vault` (
|
|
56
|
+
`fact_id` text PRIMARY KEY NOT NULL,
|
|
57
|
+
`tenant_id` text NOT NULL,
|
|
58
|
+
`stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
|
|
59
|
+
`order_id` text,
|
|
60
|
+
`placed_month` text NOT NULL CHECK (length(`placed_month`) = 6),
|
|
61
|
+
`currency` text,
|
|
62
|
+
`order_total_minor` integer,
|
|
63
|
+
`item_count` integer,
|
|
64
|
+
`ship_region` text,
|
|
65
|
+
`ship_country` text,
|
|
66
|
+
`ship_zip3` text,
|
|
67
|
+
`bill_region` text,
|
|
68
|
+
`bill_country` text,
|
|
69
|
+
`bill_zip3` text,
|
|
70
|
+
`time_scale` integer,
|
|
71
|
+
`rule_version` text,
|
|
72
|
+
`order_kid` text,
|
|
73
|
+
`bidx_kid` text,
|
|
74
|
+
`sealed_at` text,
|
|
75
|
+
`sealed_jti` text,
|
|
76
|
+
`erased_at` text,
|
|
77
|
+
`name_ct` text,
|
|
78
|
+
`company_ct` text,
|
|
79
|
+
`identity_ceiling` text,
|
|
80
|
+
`identity_expires_at` text,
|
|
81
|
+
`identity_floor_until` text,
|
|
82
|
+
`identity_rule` text,
|
|
83
|
+
`email_ct` text,
|
|
84
|
+
`email_bidx` text,
|
|
85
|
+
`email_ceiling` text,
|
|
86
|
+
`email_expires_at` text,
|
|
87
|
+
`email_floor_until` text,
|
|
88
|
+
`email_rule` text,
|
|
89
|
+
`phone_ct` text,
|
|
90
|
+
`phone_bidx` text,
|
|
91
|
+
`phone_ceiling` text,
|
|
92
|
+
`phone_expires_at` text,
|
|
93
|
+
`phone_floor_until` text,
|
|
94
|
+
`phone_rule` text,
|
|
95
|
+
`ship_name_ct` text,
|
|
96
|
+
`ship_company_ct` text,
|
|
97
|
+
`ship_line1_ct` text,
|
|
98
|
+
`ship_line2_ct` text,
|
|
99
|
+
`ship_city` text,
|
|
100
|
+
`ship_postal_code` text,
|
|
101
|
+
`ship_ceiling` text,
|
|
102
|
+
`ship_expires_at` text,
|
|
103
|
+
`ship_floor_until` text,
|
|
104
|
+
`ship_rule` text,
|
|
105
|
+
`bill_name_ct` text,
|
|
106
|
+
`bill_company_ct` text,
|
|
107
|
+
`bill_line1_ct` text,
|
|
108
|
+
`bill_line2_ct` text,
|
|
109
|
+
`bill_city` text,
|
|
110
|
+
`bill_postal_code` text,
|
|
111
|
+
`bill_ceiling` text,
|
|
112
|
+
`bill_expires_at` text,
|
|
113
|
+
`bill_floor_until` text,
|
|
114
|
+
`bill_rule` text,
|
|
115
|
+
CHECK (`identity_expires_at` IS NULL OR `identity_expires_at` <= `identity_ceiling`),
|
|
116
|
+
CHECK (`email_expires_at` IS NULL OR `email_expires_at` <= `email_ceiling`),
|
|
117
|
+
CHECK (`phone_expires_at` IS NULL OR `phone_expires_at` <= `phone_ceiling`),
|
|
118
|
+
CHECK (`ship_expires_at` IS NULL OR `ship_expires_at` <= `ship_ceiling`),
|
|
119
|
+
CHECK (`bill_expires_at` IS NULL OR `bill_expires_at` <= `bill_ceiling`),
|
|
120
|
+
FOREIGN KEY (`tenant_id`, `stage`, `order_id`) REFERENCES `scrappy_commerce_orders` (`tenant_id`, `stage`, `order_id`)
|
|
121
|
+
);
|
|
122
|
+
--> statement-breakpoint
|
|
123
|
+
CREATE UNIQUE INDEX `uq_order_contact_vault_order`
|
|
124
|
+
ON `order_contact_vault` (`tenant_id`, `stage`, `order_id`) WHERE `order_id` IS NOT NULL;
|
|
125
|
+
--> statement-breakpoint
|
|
126
|
+
CREATE INDEX `idx_order_contact_vault_email_bidx`
|
|
127
|
+
ON `order_contact_vault` (`tenant_id`, `stage`, `email_bidx`) WHERE `email_bidx` IS NOT NULL;
|
|
128
|
+
--> statement-breakpoint
|
|
129
|
+
CREATE INDEX `idx_order_contact_vault_phone_bidx`
|
|
130
|
+
ON `order_contact_vault` (`tenant_id`, `stage`, `phone_bidx`) WHERE `phone_bidx` IS NOT NULL;
|
|
131
|
+
--> statement-breakpoint
|
|
132
|
+
CREATE INDEX `idx_order_contact_vault_order_kid` ON `order_contact_vault` (`order_kid`) WHERE `order_kid` IS NOT NULL;
|
|
133
|
+
--> statement-breakpoint
|
|
134
|
+
CREATE INDEX `idx_order_contact_vault_bidx_kid` ON `order_contact_vault` (`bidx_kid`) WHERE `bidx_kid` IS NOT NULL;
|
|
135
|
+
--> statement-breakpoint
|
|
136
|
+
CREATE INDEX `idx_order_contact_vault_linked` ON `order_contact_vault` (`tenant_id`, `stage`) WHERE `order_id` IS NOT NULL;
|
|
137
|
+
--> statement-breakpoint
|
|
138
|
+
-- A group may only expire sooner (to a non-NULL value) and may only be cleared, never refilled;
|
|
139
|
+
-- its ceiling, floor and rule never change. The one exception is the detach statement (the order
|
|
140
|
+
-- link goes to NULL with every group already empty), which nulls all of them. A blind index takes
|
|
141
|
+
-- a new value only as a rotation recompute: its group still sealed with the same ciphertext and
|
|
142
|
+
-- bidx_kid moving to another kid in the same statement, replacing every index the row holds;
|
|
143
|
+
-- bidx_kid goes to NULL only with both indexes. The order link only ever goes to NULL.
|
|
144
|
+
CREATE TRIGGER `order_contact_vault_shrink_only`
|
|
145
|
+
BEFORE UPDATE ON `order_contact_vault`
|
|
146
|
+
WHEN NEW.`tenant_id` IS NOT OLD.`tenant_id`
|
|
147
|
+
OR NEW.`stage` IS NOT OLD.`stage`
|
|
148
|
+
OR NEW.`fact_id` IS NOT OLD.`fact_id`
|
|
149
|
+
OR NEW.`placed_month` IS NOT OLD.`placed_month`
|
|
150
|
+
OR (NEW.`order_id` IS NOT NULL AND NEW.`order_id` IS NOT OLD.`order_id`)
|
|
151
|
+
OR (NEW.`identity_ceiling` IS NOT OLD.`identity_ceiling` AND (NEW.`identity_ceiling` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
152
|
+
OR (NEW.`identity_floor_until` IS NOT OLD.`identity_floor_until` AND (NEW.`identity_floor_until` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
153
|
+
OR (NEW.`identity_rule` IS NOT OLD.`identity_rule` AND (NEW.`identity_rule` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
154
|
+
OR (NEW.`identity_expires_at` IS NOT OLD.`identity_expires_at` AND NOT ((NEW.`identity_expires_at` IS NULL AND (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)) OR (NEW.`identity_expires_at` IS NOT NULL AND OLD.`identity_expires_at` IS NOT NULL AND NEW.`identity_expires_at` < OLD.`identity_expires_at`)))
|
|
155
|
+
OR (NEW.`email_ceiling` IS NOT OLD.`email_ceiling` AND (NEW.`email_ceiling` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
156
|
+
OR (NEW.`email_floor_until` IS NOT OLD.`email_floor_until` AND (NEW.`email_floor_until` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
157
|
+
OR (NEW.`email_rule` IS NOT OLD.`email_rule` AND (NEW.`email_rule` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
158
|
+
OR (NEW.`email_expires_at` IS NOT OLD.`email_expires_at` AND NOT ((NEW.`email_expires_at` IS NULL AND (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)) OR (NEW.`email_expires_at` IS NOT NULL AND OLD.`email_expires_at` IS NOT NULL AND NEW.`email_expires_at` < OLD.`email_expires_at`)))
|
|
159
|
+
OR (NEW.`phone_ceiling` IS NOT OLD.`phone_ceiling` AND (NEW.`phone_ceiling` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
160
|
+
OR (NEW.`phone_floor_until` IS NOT OLD.`phone_floor_until` AND (NEW.`phone_floor_until` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
161
|
+
OR (NEW.`phone_rule` IS NOT OLD.`phone_rule` AND (NEW.`phone_rule` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
162
|
+
OR (NEW.`phone_expires_at` IS NOT OLD.`phone_expires_at` AND NOT ((NEW.`phone_expires_at` IS NULL AND (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)) OR (NEW.`phone_expires_at` IS NOT NULL AND OLD.`phone_expires_at` IS NOT NULL AND NEW.`phone_expires_at` < OLD.`phone_expires_at`)))
|
|
163
|
+
OR (NEW.`ship_ceiling` IS NOT OLD.`ship_ceiling` AND (NEW.`ship_ceiling` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
164
|
+
OR (NEW.`ship_floor_until` IS NOT OLD.`ship_floor_until` AND (NEW.`ship_floor_until` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
165
|
+
OR (NEW.`ship_rule` IS NOT OLD.`ship_rule` AND (NEW.`ship_rule` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
166
|
+
OR (NEW.`ship_expires_at` IS NOT OLD.`ship_expires_at` AND NOT ((NEW.`ship_expires_at` IS NULL AND (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)) OR (NEW.`ship_expires_at` IS NOT NULL AND OLD.`ship_expires_at` IS NOT NULL AND NEW.`ship_expires_at` < OLD.`ship_expires_at`)))
|
|
167
|
+
OR (NEW.`bill_ceiling` IS NOT OLD.`bill_ceiling` AND (NEW.`bill_ceiling` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
168
|
+
OR (NEW.`bill_floor_until` IS NOT OLD.`bill_floor_until` AND (NEW.`bill_floor_until` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
169
|
+
OR (NEW.`bill_rule` IS NOT OLD.`bill_rule` AND (NEW.`bill_rule` IS NOT NULL OR NOT (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)))
|
|
170
|
+
OR (NEW.`bill_expires_at` IS NOT OLD.`bill_expires_at` AND NOT ((NEW.`bill_expires_at` IS NULL AND (NEW.`order_id` IS NULL AND NEW.`name_ct` IS NULL AND NEW.`company_ct` IS NULL AND NEW.`email_ct` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_ct` IS NULL AND NEW.`phone_bidx` IS NULL AND NEW.`ship_name_ct` IS NULL AND NEW.`ship_company_ct` IS NULL AND NEW.`ship_line1_ct` IS NULL AND NEW.`ship_line2_ct` IS NULL AND NEW.`ship_city` IS NULL AND NEW.`ship_postal_code` IS NULL AND NEW.`bill_name_ct` IS NULL AND NEW.`bill_company_ct` IS NULL AND NEW.`bill_line1_ct` IS NULL AND NEW.`bill_line2_ct` IS NULL AND NEW.`bill_city` IS NULL AND NEW.`bill_postal_code` IS NULL)) OR (NEW.`bill_expires_at` IS NOT NULL AND OLD.`bill_expires_at` IS NOT NULL AND NEW.`bill_expires_at` < OLD.`bill_expires_at`)))
|
|
171
|
+
OR (NEW.`name_ct` IS NOT NULL AND NEW.`name_ct` IS NOT OLD.`name_ct`)
|
|
172
|
+
OR (NEW.`company_ct` IS NOT NULL AND NEW.`company_ct` IS NOT OLD.`company_ct`)
|
|
173
|
+
OR (NEW.`email_ct` IS NOT NULL AND NEW.`email_ct` IS NOT OLD.`email_ct`)
|
|
174
|
+
OR (NEW.`phone_ct` IS NOT NULL AND NEW.`phone_ct` IS NOT OLD.`phone_ct`)
|
|
175
|
+
OR (NEW.`ship_name_ct` IS NOT NULL AND NEW.`ship_name_ct` IS NOT OLD.`ship_name_ct`)
|
|
176
|
+
OR (NEW.`ship_company_ct` IS NOT NULL AND NEW.`ship_company_ct` IS NOT OLD.`ship_company_ct`)
|
|
177
|
+
OR (NEW.`ship_line1_ct` IS NOT NULL AND NEW.`ship_line1_ct` IS NOT OLD.`ship_line1_ct`)
|
|
178
|
+
OR (NEW.`ship_line2_ct` IS NOT NULL AND NEW.`ship_line2_ct` IS NOT OLD.`ship_line2_ct`)
|
|
179
|
+
OR (NEW.`ship_city` IS NOT NULL AND NEW.`ship_city` IS NOT OLD.`ship_city`)
|
|
180
|
+
OR (NEW.`ship_postal_code` IS NOT NULL AND NEW.`ship_postal_code` IS NOT OLD.`ship_postal_code`)
|
|
181
|
+
OR (NEW.`bill_name_ct` IS NOT NULL AND NEW.`bill_name_ct` IS NOT OLD.`bill_name_ct`)
|
|
182
|
+
OR (NEW.`bill_company_ct` IS NOT NULL AND NEW.`bill_company_ct` IS NOT OLD.`bill_company_ct`)
|
|
183
|
+
OR (NEW.`bill_line1_ct` IS NOT NULL AND NEW.`bill_line1_ct` IS NOT OLD.`bill_line1_ct`)
|
|
184
|
+
OR (NEW.`bill_line2_ct` IS NOT NULL AND NEW.`bill_line2_ct` IS NOT OLD.`bill_line2_ct`)
|
|
185
|
+
OR (NEW.`bill_city` IS NOT NULL AND NEW.`bill_city` IS NOT OLD.`bill_city`)
|
|
186
|
+
OR (NEW.`bill_postal_code` IS NOT NULL AND NEW.`bill_postal_code` IS NOT OLD.`bill_postal_code`)
|
|
187
|
+
OR (NEW.`email_bidx` IS NOT NULL AND NEW.`email_bidx` IS NOT OLD.`email_bidx` AND NOT (
|
|
188
|
+
OLD.`email_bidx` IS NOT NULL AND OLD.`email_ct` IS NOT NULL AND NEW.`email_ct` IS OLD.`email_ct`
|
|
189
|
+
AND OLD.`bidx_kid` IS NOT NULL AND NEW.`bidx_kid` IS NOT NULL AND NEW.`bidx_kid` IS NOT OLD.`bidx_kid`))
|
|
190
|
+
OR (NEW.`phone_bidx` IS NOT NULL AND NEW.`phone_bidx` IS NOT OLD.`phone_bidx` AND NOT (
|
|
191
|
+
OLD.`phone_bidx` IS NOT NULL AND OLD.`phone_ct` IS NOT NULL AND NEW.`phone_ct` IS OLD.`phone_ct`
|
|
192
|
+
AND OLD.`bidx_kid` IS NOT NULL AND NEW.`bidx_kid` IS NOT NULL AND NEW.`bidx_kid` IS NOT OLD.`bidx_kid`))
|
|
193
|
+
OR (NEW.`order_kid` IS NOT NULL AND NEW.`order_kid` IS NOT OLD.`order_kid`)
|
|
194
|
+
OR (NEW.`bidx_kid` IS NOT OLD.`bidx_kid` AND NOT (
|
|
195
|
+
(NEW.`bidx_kid` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_bidx` IS NULL)
|
|
196
|
+
OR (NEW.`bidx_kid` IS NOT NULL AND OLD.`bidx_kid` IS NOT NULL
|
|
197
|
+
AND (OLD.`email_bidx` IS NULL OR NEW.`email_bidx` IS NOT OLD.`email_bidx`)
|
|
198
|
+
AND (OLD.`phone_bidx` IS NULL OR NEW.`phone_bidx` IS NOT OLD.`phone_bidx`)
|
|
199
|
+
AND (NEW.`email_bidx` IS NOT NULL OR NEW.`phone_bidx` IS NOT NULL))))
|
|
200
|
+
BEGIN
|
|
201
|
+
SELECT RAISE(ABORT, 'vault-immutable');
|
|
202
|
+
END;
|
|
203
|
+
--> statement-breakpoint
|
|
204
|
+
CREATE TABLE `order_contact_markers` (
|
|
205
|
+
`tenant_id` text NOT NULL,
|
|
206
|
+
`stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
|
|
207
|
+
`order_id` text NOT NULL,
|
|
208
|
+
PRIMARY KEY (`tenant_id`, `stage`, `order_id`),
|
|
209
|
+
FOREIGN KEY (`tenant_id`, `stage`, `order_id`) REFERENCES `scrappy_commerce_orders` (`tenant_id`, `stage`, `order_id`)
|
|
210
|
+
);
|
|
211
|
+
--> statement-breakpoint
|
|
212
|
+
CREATE TABLE `contact_access_audit` (
|
|
213
|
+
`audit_id` text NOT NULL PRIMARY KEY,
|
|
214
|
+
`tenant_id` text NOT NULL,
|
|
215
|
+
`stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
|
|
216
|
+
`record_kind` text NOT NULL CHECK (`record_kind` IN ('order-contact', 'contact-message')),
|
|
217
|
+
`record_id` text,
|
|
218
|
+
`record_count` integer,
|
|
219
|
+
`at` text NOT NULL,
|
|
220
|
+
`actor_id` text NOT NULL,
|
|
221
|
+
`actor_role` text NOT NULL CHECK (`actor_role` IN ('owner', 'staff')),
|
|
222
|
+
`reason_code` text,
|
|
223
|
+
`ticket_ref` text,
|
|
224
|
+
`action` text NOT NULL CHECK (`action` IN ('read', 'list', 'erase', 'search')),
|
|
225
|
+
`groups` text,
|
|
226
|
+
`outcome` text NOT NULL CHECK (`outcome` = 'admitted'),
|
|
227
|
+
CHECK (`actor_role` = 'owner' OR (`reason_code` IS NOT NULL AND `ticket_ref` IS NOT NULL))
|
|
228
|
+
);
|
|
229
|
+
--> statement-breakpoint
|
|
230
|
+
CREATE INDEX `idx_contact_access_audit_quota` ON `contact_access_audit` (`tenant_id`, `actor_id`, `actor_role`, `at`);
|
|
231
|
+
--> statement-breakpoint
|
|
232
|
+
CREATE TRIGGER `contact_access_audit_no_update`
|
|
233
|
+
BEFORE UPDATE ON `contact_access_audit`
|
|
234
|
+
BEGIN
|
|
235
|
+
SELECT RAISE(ABORT, 'append-only');
|
|
236
|
+
END;
|
|
237
|
+
--> statement-breakpoint
|
|
238
|
+
CREATE TRIGGER `contact_access_audit_no_delete`
|
|
239
|
+
BEFORE DELETE ON `contact_access_audit`
|
|
240
|
+
BEGIN
|
|
241
|
+
SELECT RAISE(ABORT, 'append-only');
|
|
242
|
+
END;
|
|
243
|
+
--> statement-breakpoint
|
|
244
|
+
-- contact_messages: every row is kept; legacy plaintext rows stay in legacy_* until the rollout
|
|
245
|
+
-- seals them field by field. New rows are written sealed only.
|
|
246
|
+
CREATE TABLE `contact_messages_v2` (
|
|
247
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
248
|
+
`tenant_id` text NOT NULL,
|
|
249
|
+
`stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
|
|
250
|
+
`msg_kid` text,
|
|
251
|
+
`bidx_kid` text,
|
|
252
|
+
`name_ct` text,
|
|
253
|
+
`email_ct` text,
|
|
254
|
+
`email_bidx` text,
|
|
255
|
+
`phone_ct` text,
|
|
256
|
+
`phone_bidx` text,
|
|
257
|
+
`message_ct` text,
|
|
258
|
+
`ceiling` text NOT NULL,
|
|
259
|
+
`expires_at` text NOT NULL CHECK (`expires_at` <= `ceiling`),
|
|
260
|
+
`time_scale` integer,
|
|
261
|
+
`rule_version` text,
|
|
262
|
+
`page_path` text,
|
|
263
|
+
`status` text NOT NULL DEFAULT 'unread' CHECK (`status` IN ('unread', 'read')),
|
|
264
|
+
`created_at` text NOT NULL,
|
|
265
|
+
`read_at` text,
|
|
266
|
+
`notify_status` text NOT NULL DEFAULT 'pending' CHECK (`notify_status` IN ('pending', 'sent', 'failed')),
|
|
267
|
+
`notify_attempts` integer NOT NULL DEFAULT 0,
|
|
268
|
+
`legacy_name` text,
|
|
269
|
+
`legacy_email` text,
|
|
270
|
+
`legacy_phone` text,
|
|
271
|
+
`legacy_message` text,
|
|
272
|
+
CHECK (`msg_kid` IS NULL OR (`legacy_name` IS NULL AND `legacy_email` IS NULL AND `legacy_phone` IS NULL AND `legacy_message` IS NULL))
|
|
273
|
+
);
|
|
274
|
+
--> statement-breakpoint
|
|
275
|
+
INSERT INTO `contact_messages_v2`
|
|
276
|
+
(`id`, `tenant_id`, `stage`, `ceiling`, `expires_at`, `page_path`, `status`, `created_at`, `read_at`,
|
|
277
|
+
`notify_status`, `notify_attempts`, `legacy_name`, `legacy_email`, `legacy_phone`, `legacy_message`)
|
|
278
|
+
SELECT `id`, `tenant_id`, `stage`, `not_after`, `expires_at`, `page_path`, `status`, `created_at`, `read_at`,
|
|
279
|
+
`notify_status`, `notify_attempts`, `legacy_name`, `legacy_email`, `legacy_phone`, `legacy_message`
|
|
280
|
+
FROM `contact_messages`;
|
|
281
|
+
--> statement-breakpoint
|
|
282
|
+
DROP TABLE `contact_messages`;
|
|
283
|
+
--> statement-breakpoint
|
|
284
|
+
ALTER TABLE `contact_messages_v2` RENAME TO `contact_messages`;
|
|
285
|
+
--> statement-breakpoint
|
|
286
|
+
CREATE INDEX `idx_contact_messages_tenant_stage_created` ON `contact_messages` (`tenant_id`, `stage`, `created_at`);
|
|
287
|
+
--> statement-breakpoint
|
|
288
|
+
CREATE INDEX `idx_contact_messages_tenant_stage_status` ON `contact_messages` (`tenant_id`, `stage`, `status`);
|
|
289
|
+
--> statement-breakpoint
|
|
290
|
+
CREATE INDEX `idx_contact_messages_expires` ON `contact_messages` (`expires_at`);
|
|
291
|
+
--> statement-breakpoint
|
|
292
|
+
CREATE INDEX `idx_contact_messages_legacy` ON `contact_messages` (`id`) WHERE `msg_kid` IS NULL AND `legacy_message` IS NOT NULL;
|
|
293
|
+
--> statement-breakpoint
|
|
294
|
+
CREATE INDEX `idx_contact_messages_email_bidx`
|
|
295
|
+
ON `contact_messages` (`tenant_id`, `stage`, `email_bidx`) WHERE `email_bidx` IS NOT NULL;
|
|
296
|
+
--> statement-breakpoint
|
|
297
|
+
CREATE INDEX `idx_contact_messages_phone_bidx`
|
|
298
|
+
ON `contact_messages` (`tenant_id`, `stage`, `phone_bidx`) WHERE `phone_bidx` IS NOT NULL;
|
|
299
|
+
--> statement-breakpoint
|
|
300
|
+
CREATE TRIGGER `contact_messages_no_plaintext_insert`
|
|
301
|
+
BEFORE INSERT ON `contact_messages`
|
|
302
|
+
WHEN NEW.`legacy_name` IS NOT NULL OR NEW.`legacy_email` IS NOT NULL
|
|
303
|
+
OR NEW.`legacy_phone` IS NOT NULL OR NEW.`legacy_message` IS NOT NULL
|
|
304
|
+
BEGIN
|
|
305
|
+
SELECT RAISE(ABORT, 'plaintext-refused');
|
|
306
|
+
END;
|
|
307
|
+
--> statement-breakpoint
|
|
308
|
+
-- Once a row is sealed (msg_kid set) its kid never changes, not even to NULL; its ciphertexts only
|
|
309
|
+
-- go to NULL; a blind index takes a new value only as a rotation recompute (bidx_kid moving to
|
|
310
|
+
-- another kid in the same statement, replacing every index the row holds); and bidx_kid goes to
|
|
311
|
+
-- NULL only with both indexes.
|
|
312
|
+
CREATE TRIGGER `contact_messages_no_plaintext_update`
|
|
313
|
+
BEFORE UPDATE ON `contact_messages`
|
|
314
|
+
WHEN (NEW.`legacy_name` IS NOT NULL AND NEW.`legacy_name` IS NOT OLD.`legacy_name`)
|
|
315
|
+
OR (NEW.`legacy_email` IS NOT NULL AND NEW.`legacy_email` IS NOT OLD.`legacy_email`)
|
|
316
|
+
OR (NEW.`legacy_phone` IS NOT NULL AND NEW.`legacy_phone` IS NOT OLD.`legacy_phone`)
|
|
317
|
+
OR (NEW.`legacy_message` IS NOT NULL AND NEW.`legacy_message` IS NOT OLD.`legacy_message`)
|
|
318
|
+
OR NEW.`expires_at` > OLD.`expires_at`
|
|
319
|
+
OR NEW.`ceiling` IS NOT OLD.`ceiling`
|
|
320
|
+
OR (OLD.`msg_kid` IS NOT NULL AND NEW.`msg_kid` IS NOT OLD.`msg_kid`)
|
|
321
|
+
OR (OLD.`msg_kid` IS NOT NULL AND (
|
|
322
|
+
(NEW.`name_ct` IS NOT NULL AND NEW.`name_ct` IS NOT OLD.`name_ct`)
|
|
323
|
+
OR (NEW.`email_ct` IS NOT NULL AND NEW.`email_ct` IS NOT OLD.`email_ct`)
|
|
324
|
+
OR (NEW.`phone_ct` IS NOT NULL AND NEW.`phone_ct` IS NOT OLD.`phone_ct`)
|
|
325
|
+
OR (NEW.`message_ct` IS NOT NULL AND NEW.`message_ct` IS NOT OLD.`message_ct`)
|
|
326
|
+
OR (NEW.`email_bidx` IS NOT NULL AND NEW.`email_bidx` IS NOT OLD.`email_bidx` AND NOT (
|
|
327
|
+
OLD.`email_bidx` IS NOT NULL AND OLD.`email_ct` IS NOT NULL AND NEW.`email_ct` IS OLD.`email_ct`
|
|
328
|
+
AND OLD.`bidx_kid` IS NOT NULL AND NEW.`bidx_kid` IS NOT NULL AND NEW.`bidx_kid` IS NOT OLD.`bidx_kid`))
|
|
329
|
+
OR (NEW.`phone_bidx` IS NOT NULL AND NEW.`phone_bidx` IS NOT OLD.`phone_bidx` AND NOT (
|
|
330
|
+
OLD.`phone_bidx` IS NOT NULL AND OLD.`phone_ct` IS NOT NULL AND NEW.`phone_ct` IS OLD.`phone_ct`
|
|
331
|
+
AND OLD.`bidx_kid` IS NOT NULL AND NEW.`bidx_kid` IS NOT NULL AND NEW.`bidx_kid` IS NOT OLD.`bidx_kid`))
|
|
332
|
+
OR (NEW.`bidx_kid` IS NOT OLD.`bidx_kid` AND NOT (
|
|
333
|
+
(NEW.`bidx_kid` IS NULL AND NEW.`email_bidx` IS NULL AND NEW.`phone_bidx` IS NULL)
|
|
334
|
+
OR (NEW.`bidx_kid` IS NOT NULL AND OLD.`bidx_kid` IS NOT NULL
|
|
335
|
+
AND (OLD.`email_bidx` IS NULL OR NEW.`email_bidx` IS NOT OLD.`email_bidx`)
|
|
336
|
+
AND (OLD.`phone_bidx` IS NULL OR NEW.`phone_bidx` IS NOT OLD.`phone_bidx`)
|
|
337
|
+
AND (NEW.`email_bidx` IS NOT NULL OR NEW.`phone_bidx` IS NOT NULL))))))
|
|
338
|
+
OR NEW.`tenant_id` IS NOT OLD.`tenant_id`
|
|
339
|
+
OR NEW.`stage` IS NOT OLD.`stage`
|
|
340
|
+
BEGIN
|
|
341
|
+
SELECT RAISE(ABORT, 'vault-immutable');
|
|
342
|
+
END;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"preview": "wrangler dev",
|
|
15
15
|
"typecheck": "astro check && tsc --noEmit",
|
|
16
16
|
"astro": "astro",
|
|
17
|
-
"test": "NODE_OPTIONS=--experimental-sqlite
|
|
17
|
+
"test": "NODE_OPTIONS=--experimental-sqlite tot-gate vitest run --exclude src/lib/integrations/apps/v84CleanCloneReplay.test.ts --exclude src/lib/integrations/apps/v84NonportableRouteDogfood.test.ts --exclude src/lib/integrations/apps/v84CloneLifecycleEvidence.test.ts && NODE_OPTIONS=--experimental-sqlite tot-gate vitest run src/lib/integrations/apps/v84CleanCloneReplay.test.ts src/lib/integrations/apps/v84NonportableRouteDogfood.test.ts src/lib/integrations/apps/v84CloneLifecycleEvidence.test.ts --fileParallelism=false && pnpm run test:pipeline && pnpm run test:pipeline-faults",
|
|
18
18
|
"depcruise": "depcruise src --config .runner-dependency-cruiser.cjs"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind indexes for exact-match contact search: base64url HMAC-SHA256 of a normalized value under
|
|
3
|
+
* the tenant's blind-index data key, so equal values match within one tenant and stage and never
|
|
4
|
+
* across tenants. Normalization is part of the index contract: changing it orphans every stored
|
|
5
|
+
* index.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Trim, NFKC, lowercase. Null when nothing remains. */
|
|
9
|
+
export function normalizeEmail(value: string): string | null {
|
|
10
|
+
const normalized = value.trim().normalize("NFKC").toLowerCase();
|
|
11
|
+
return normalized === "" ? null : normalized;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** NFKC, then a leading '+' (if the trimmed value starts with one) and ASCII digits only. Null when no digit remains. */
|
|
15
|
+
export function normalizePhone(value: string): string | null {
|
|
16
|
+
const folded = value.normalize("NFKC").trim();
|
|
17
|
+
const digits = folded.replace(/[^0-9]/g, "");
|
|
18
|
+
if (digits === "") return null;
|
|
19
|
+
return folded.startsWith("+") ? `+${digits}` : digits;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** base64url (no padding) HMAC-SHA256 of an already-normalized value. */
|
|
23
|
+
export async function blindIndex(hmacKey: CryptoKey, normalized: string): Promise<string> {
|
|
24
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, new TextEncoder().encode(normalized) as BufferSource));
|
|
25
|
+
let binary = "";
|
|
26
|
+
for (const byte of mac) binary += String.fromCharCode(byte);
|
|
27
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
28
|
+
}
|
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
* The contact access audit (`contact_access_audit`, append-only): every read, list and erase of
|
|
3
3
|
* sealed customer contact writes its row BEFORE anything is decrypted. Staff rows carry the
|
|
4
4
|
* 30-per-hour quota in the same statement, so concurrent reads can never pass it together.
|
|
5
|
-
* Rows hold opaque ids, codes and times only — never a contact field.
|
|
5
|
+
* Rows hold opaque ids, codes and times only — never a contact field or a searched value. A list
|
|
6
|
+
* or search row names the record ids it returned (comma-joined in record_id, at most
|
|
7
|
+
* {@link AUDIT_RECORD_IDS_MAX}) and their count; an erase row names the field groups it cleared.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
export type VaultStage = "preview" | "publication";
|
|
9
11
|
|
|
10
12
|
export const STAFF_READS_PER_HOUR = 30;
|
|
13
|
+
/** The most ids one list or search audit row names; every list and search is capped at or below it. */
|
|
14
|
+
export const AUDIT_RECORD_IDS_MAX = 200;
|
|
15
|
+
|
|
16
|
+
/** A list or search row's record_id: the returned ids comma-joined, or null when it returned none. */
|
|
17
|
+
export function auditRecordIds(ids: readonly string[]): string | null {
|
|
18
|
+
if (ids.length > AUDIT_RECORD_IDS_MAX) throw new Error("contact audit: too many record ids");
|
|
19
|
+
return ids.length === 0 ? null : ids.join(",");
|
|
20
|
+
}
|
|
11
21
|
const HOUR_MS = 3_600_000;
|
|
12
22
|
|
|
13
23
|
export interface ContactActor {
|
|
@@ -36,27 +46,31 @@ export function auditStatement(input: {
|
|
|
36
46
|
tenantId: string;
|
|
37
47
|
stage: VaultStage;
|
|
38
48
|
recordKind: "order-contact" | "contact-message";
|
|
39
|
-
recordId: string;
|
|
49
|
+
recordId: string | null;
|
|
50
|
+
recordCount?: number;
|
|
51
|
+
groups?: readonly string[];
|
|
40
52
|
actor: ContactActor;
|
|
41
|
-
action: "read" | "list" | "erase";
|
|
53
|
+
action: "read" | "list" | "erase" | "search";
|
|
42
54
|
now: Date;
|
|
43
55
|
requireRow?: AuditStatement;
|
|
44
56
|
}): AuditStatement {
|
|
45
57
|
const { auditId, tenantId, stage, recordKind, recordId, actor, action, now } = input;
|
|
58
|
+
const recordCount = input.recordCount ?? null;
|
|
59
|
+
const groups = input.groups && input.groups.length > 0 ? [...input.groups].sort().join(",") : null;
|
|
46
60
|
const hourAgo = new Date(now.getTime() - HOUR_MS).toISOString();
|
|
47
61
|
const params: unknown[] = [
|
|
48
|
-
auditId, tenantId, stage, recordKind, recordId, now.toISOString(), actor.actorId, actor.role,
|
|
62
|
+
auditId, tenantId, stage, recordKind, recordId, recordCount, now.toISOString(), actor.actorId, actor.role,
|
|
49
63
|
actor.role === "staff" ? actor.reasonCode ?? null : null,
|
|
50
64
|
actor.role === "staff" ? actor.ticketRef ?? null : null,
|
|
51
|
-
action,
|
|
65
|
+
action, groups,
|
|
52
66
|
actor.role, tenantId, actor.actorId, hourAgo, STAFF_READS_PER_HOUR,
|
|
53
67
|
];
|
|
54
68
|
const requireRow = input.requireRow ? ` AND EXISTS (${input.requireRow.sql})` : "";
|
|
55
69
|
if (input.requireRow) params.push(...input.requireRow.params);
|
|
56
70
|
return {
|
|
57
71
|
sql: `INSERT INTO contact_access_audit
|
|
58
|
-
(audit_id, tenant_id, stage, record_kind, record_id, at, actor_id, actor_role, reason_code, ticket_ref, action, outcome)
|
|
59
|
-
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'admitted'
|
|
72
|
+
(audit_id, tenant_id, stage, record_kind, record_id, record_count, at, actor_id, actor_role, reason_code, ticket_ref, action, groups, outcome)
|
|
73
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'admitted'
|
|
60
74
|
WHERE (? = 'owner' OR (SELECT COUNT(*) FROM contact_access_audit
|
|
61
75
|
WHERE tenant_id = ? AND actor_id = ? AND actor_role = 'staff' AND at > ?) < ?)${requireRow}
|
|
62
76
|
RETURNING audit_id`,
|