@12-apps/prisma 6.5.0 → 6.7.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.
- package/package.json +8 -8
- package/prisma/migrations/20260830120000_discount_weekly_schedule/migration.sql +40 -0
- package/prisma/migrations/20260910120000_add_refresh_grace_seal/migration.sql +37 -0
- package/prisma/plugin-migrations.json +3 -1
- package/prisma/schema/discounts.prisma +23 -0
- package/prisma/schema/mcp.prisma +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/prisma",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Prisma host: the multi-file schema folder, the plugin migration seam, and the shared PrismaClient singleton with its audit / append-only extensions",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -68,22 +68,22 @@
|
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@12-apps/audit": "^5.7.1",
|
|
71
|
-
"@12-apps/auth": "^2.
|
|
72
|
-
"@12-apps/discounts": "^1.
|
|
71
|
+
"@12-apps/auth": "^2.21.0",
|
|
72
|
+
"@12-apps/discounts": "^1.13.0",
|
|
73
73
|
"@12-apps/entitlements": "^3.10.0",
|
|
74
74
|
"@12-apps/entity-lifecycle": "^4.13.0",
|
|
75
75
|
"@12-apps/eslint-config": "^1.22.0",
|
|
76
76
|
"@12-apps/feature-flags": "^2.4.1",
|
|
77
77
|
"@12-apps/i18n": "^1.3.0",
|
|
78
78
|
"@12-apps/jobs": "^4.5.0",
|
|
79
|
-
"@12-apps/mcp": "^3.
|
|
80
|
-
"@12-apps/notifications": "^4.
|
|
79
|
+
"@12-apps/mcp": "^3.17.0",
|
|
80
|
+
"@12-apps/notifications": "^4.11.0",
|
|
81
81
|
"@12-apps/onboarding": "^2.7.0",
|
|
82
|
-
"@12-apps/payments-backend": "^4.
|
|
82
|
+
"@12-apps/payments-backend": "^4.27.1",
|
|
83
83
|
"@12-apps/product-research": "^2.10.0",
|
|
84
|
-
"@12-apps/rbac": "^4.
|
|
84
|
+
"@12-apps/rbac": "^4.14.0",
|
|
85
85
|
"@12-apps/realtime": "^2.6.0",
|
|
86
|
-
"@12-apps/report-builder": "^5.14.
|
|
86
|
+
"@12-apps/report-builder": "^5.14.1",
|
|
87
87
|
"@12-apps/shift": "^3.4.0",
|
|
88
88
|
"@12-apps/typescript-config": "^1.21.0",
|
|
89
89
|
"@types/node": "^22.10.6",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- @12-apps/discounts: the WEEKLY schedule (FUT-996).
|
|
2
|
+
--
|
|
3
|
+
-- "Toda sexta, das 16:00 às 20:00" — a recurring window INSIDE the campaign
|
|
4
|
+
-- `[starts_at, ends_at)`, which until now was the only scheduling a rule had.
|
|
5
|
+
--
|
|
6
|
+
-- ── PURELY ADDITIVE, AND THAT IS THE POINT ────────────────────────────────
|
|
7
|
+
-- One nullable column, no backfill, no CHECK to widen, nothing dropped. NULL
|
|
8
|
+
-- means "always, within the campaign window", which is exactly what every
|
|
9
|
+
-- existing row already means — so this migration cannot change the price of
|
|
10
|
+
-- anything, and the release carrying it needs none of the expand/contract
|
|
11
|
+
-- ceremony a destructive DDL would (see the host's CLAUDE.md on why a
|
|
12
|
+
-- `DROP COLUMN` takes production down for the length of a healthcheck).
|
|
13
|
+
--
|
|
14
|
+
-- Replay-safe like its predecessor: `IF NOT EXISTS` throughout.
|
|
15
|
+
--
|
|
16
|
+
-- ── WHY JSON RATHER THAN A FOURTH TABLE ───────────────────────────────────
|
|
17
|
+
-- The recurrence is not SQL-queryable in any useful way. The admin grid's
|
|
18
|
+
-- vigência filter already punts on comparing two nullable timestamps against
|
|
19
|
+
-- `now()` and rides as a separate parameter the store resolves, so a schedule
|
|
20
|
+
-- table would buy queryability that nothing asks for — while adding a third
|
|
21
|
+
-- `include` to `loadDiscountRules`, which is on the storefront's hottest read
|
|
22
|
+
-- (a whole menu page's badge preview walks it once per request).
|
|
23
|
+
--
|
|
24
|
+
-- The trade is stated plainly: the database cannot check this column's SHAPE.
|
|
25
|
+
-- `assertSchedule` in `src/server/validate.ts` is the only gate in front of it,
|
|
26
|
+
-- and the evaluator is deliberately tolerant of a row that got past it — a
|
|
27
|
+
-- malformed window covers NOTHING rather than everything, so the failure mode
|
|
28
|
+
-- of a bad blob is a promotion that does not fire, never a cart priced at zero.
|
|
29
|
+
--
|
|
30
|
+
-- Shape, for a reader with only the database in front of them:
|
|
31
|
+
--
|
|
32
|
+
-- {"windows":[{"days":[4],"from":"16:00","to":"20:00"}]}
|
|
33
|
+
--
|
|
34
|
+
-- * `days` — Monday-first, 0..6. NOT `Date#getDay()`'s Sunday-first axis.
|
|
35
|
+
-- * `from`/`to` — "HH:MM", 24-hour, in the STORE's timezone, half-open
|
|
36
|
+
-- [from, to) exactly as [starts_at, ends_at) is.
|
|
37
|
+
-- * `to` < `from` means the window runs past midnight (a bar shutting at
|
|
38
|
+
-- 02:00), which is the most common happy hour there is.
|
|
39
|
+
|
|
40
|
+
ALTER TABLE "discounts" ADD COLUMN IF NOT EXISTS "schedule" JSONB;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- @12-apps/mcp: the sealed successor that makes refresh rotation RETRYABLE.
|
|
2
|
+
--
|
|
3
|
+
-- `oauth_refresh_tokens.grace_seal` holds a token's own plaintext, encrypted
|
|
4
|
+
-- (AES-256-GCM) under a key derived by HKDF from the plaintext of the token it
|
|
5
|
+
-- was rotated FROM, with its grace deadline sealed inside the same blob.
|
|
6
|
+
--
|
|
7
|
+
-- WHY THE COLUMN EXISTS. Rotation-on-use with replay revocation cannot tell a
|
|
8
|
+
-- thief from a client that used one token twice for an innocent reason, and
|
|
9
|
+
-- there are two routine innocent reasons: a response lost to a proxy timeout,
|
|
10
|
+
-- and two of the client's own sessions refreshing at the same moment. Both were
|
|
11
|
+
-- punished as theft — the whole lineage revoked, including the successor just
|
|
12
|
+
-- handed to whoever won — which killed a live connection and sent a human back
|
|
13
|
+
-- through the full authorization flow. With this column, re-presenting a
|
|
14
|
+
-- just-consumed token inside the window returns THAT SAME successor instead, so
|
|
15
|
+
-- one successor is still all that ever exists and no second family is created.
|
|
16
|
+
--
|
|
17
|
+
-- WHY IT IS NOT A PLAINTEXT COLUMN. The key is never stored: it is derived from
|
|
18
|
+
-- the parent, which is itself only ever stored hashed. A dump of this table
|
|
19
|
+
-- therefore yields ciphertext and nothing that opens it, so the package's
|
|
20
|
+
-- "hashed, never plaintext" invariant is unchanged. The only party that can open
|
|
21
|
+
-- a seal is one presenting the parent — which is the party being served, and
|
|
22
|
+
-- which already held the token that mints the successor.
|
|
23
|
+
--
|
|
24
|
+
-- The seal is CLEARED whenever a token is consumed or revoked, and that is the
|
|
25
|
+
-- bound on what it costs: a seal opens only under the plaintext it was rotated
|
|
26
|
+
-- from, so spent seals left in place would chain — one historical plaintext plus
|
|
27
|
+
-- a copy of this table would walk forward to the live token offline, with no
|
|
28
|
+
-- server call and therefore no replay detection.
|
|
29
|
+
--
|
|
30
|
+
-- Nullable, but NOT optional: the package writes this field on every rotation,
|
|
31
|
+
-- so a deployment that raises the package version without applying this
|
|
32
|
+
-- migration gets a runtime failure on every refresh, not a quietly disabled
|
|
33
|
+
-- window. Apply it in the same change as the version raise. Guarded with
|
|
34
|
+
-- IF NOT EXISTS like every other statement this package ships, so a host that
|
|
35
|
+
-- already added the column adopts the migration as a no-op.
|
|
36
|
+
ALTER TABLE "oauth_refresh_tokens"
|
|
37
|
+
ADD COLUMN IF NOT EXISTS "grace_seal" TEXT;
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"20260821120000_shift_kind_host_vocabulary",
|
|
38
38
|
"20260821140000_discounts_package_owned",
|
|
39
39
|
"20260824120000_payments_pending_verification",
|
|
40
|
-
"20260826170000_locale_preferences"
|
|
40
|
+
"20260826170000_locale_preferences",
|
|
41
|
+
"20260830120000_discount_weekly_schedule",
|
|
42
|
+
"20260910120000_add_refresh_grace_seal"
|
|
41
43
|
]
|
|
42
44
|
}
|
|
@@ -79,6 +79,29 @@ model Discount {
|
|
|
79
79
|
// either side = open-ended.
|
|
80
80
|
startsAt DateTime? @map("starts_at")
|
|
81
81
|
endsAt DateTime? @map("ends_at")
|
|
82
|
+
// The WEEKLY schedule INSIDE that window (FUT-996) — "toda sexta, das 16:00
|
|
83
|
+
// às 20:00", "segunda e terça à tarde". NULL = always, within
|
|
84
|
+
// [starts_at, ends_at), which is what every row predating the feature means.
|
|
85
|
+
//
|
|
86
|
+
// {"windows":[{"days":[4],"from":"16:00","to":"20:00"}]}
|
|
87
|
+
//
|
|
88
|
+
// `days` is Monday-first 0..6 — the axis every hours surface uses, and NOT
|
|
89
|
+
// `Date#getDay()`'s Sunday-first one. `from`/`to` are "HH:MM" in the STORE's
|
|
90
|
+
// timezone, half-open [from, to) exactly as the campaign window is, and a
|
|
91
|
+
// `to` earlier than `from` runs past midnight rather than being invalid.
|
|
92
|
+
//
|
|
93
|
+
// JSON rather than a fourth table because the recurrence is not usefully
|
|
94
|
+
// SQL-queryable (the grid's vigência filter already cannot express the
|
|
95
|
+
// campaign window either), and a fourth table would put a third `include` on
|
|
96
|
+
// the storefront's hottest read. The cost is that the DATABASE cannot check
|
|
97
|
+
// this shape: `assertSchedule` is the only gate, and the evaluator treats a
|
|
98
|
+
// malformed window as covering NOTHING, so a bad blob is a promotion that
|
|
99
|
+
// does not fire rather than a cart priced wrong.
|
|
100
|
+
//
|
|
101
|
+
// A recurring window is a WALL-CLOCK fact, so it is only meaningful against
|
|
102
|
+
// the store's own timezone. The engine never reads one: a host resolves the
|
|
103
|
+
// instant into a weekday and a minute-of-day and passes those in.
|
|
104
|
+
schedule Json? @map("schedule")
|
|
82
105
|
// Minimum PRE-DISCOUNT cart subtotal, in cents. Always compared against the
|
|
83
106
|
// untouched subtotal, never a running total — which is what makes the result
|
|
84
107
|
// independent of the order the discounts are applied in.
|
package/prisma/schema/mcp.prisma
CHANGED
|
@@ -71,6 +71,16 @@ model OAuthRefreshToken {
|
|
|
71
71
|
revokedAt DateTime? @map("revoked_at")
|
|
72
72
|
createdAt DateTime @default(now()) @map("created_at")
|
|
73
73
|
|
|
74
|
+
// This token's own plaintext, SEALED under a key derived from the plaintext of
|
|
75
|
+
// the token it was rotated from, with its grace deadline inside the sealed
|
|
76
|
+
// blob. It is what lets a rotation be RETRIED: re-presenting the consumed
|
|
77
|
+
// parent within the window returns this same successor instead of revoking the
|
|
78
|
+
// lineage, so a lost response or two concurrent refreshes no longer end the
|
|
79
|
+
// connection. Only a caller holding the parent can open it, so the "never store
|
|
80
|
+
// a token in the clear" rule is intact — a dump of this table yields ciphertext
|
|
81
|
+
// and no key. Null on a root token, and whenever the window is turned off.
|
|
82
|
+
graceSeal String? @map("grace_seal")
|
|
83
|
+
|
|
74
84
|
// `tokenHash` is already indexed by its `@unique` constraint — the
|
|
75
85
|
// lookup-on-presentation path — so no separate `@@index([tokenHash])` is added
|
|
76
86
|
// (it would be redundant). The composite index serves per-user/per-client
|