@ttt-productions/ttt-core 0.79.79 → 0.79.81
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/dist/doc-schemas/user.d.ts +7 -0
- package/dist/doc-schemas/user.d.ts.map +1 -1
- package/dist/doc-schemas/user.js +9 -0
- package/dist/doc-schemas/user.js.map +1 -1
- package/dist/utils/app-config.d.ts +25 -12
- package/dist/utils/app-config.d.ts.map +1 -1
- package/dist/utils/app-config.js +24 -18
- package/dist/utils/app-config.js.map +1 -1
- package/package.json +3 -3
|
@@ -8,6 +8,13 @@ export declare const UserAccountStatusSchema: z.ZodEnum<{
|
|
|
8
8
|
banned: "banned";
|
|
9
9
|
}>;
|
|
10
10
|
export type UserAccountStatus = z.infer<typeof UserAccountStatusSchema>;
|
|
11
|
+
/** The status an account holds while no status decision has ever been written: a profile, or a
|
|
12
|
+
* token, with no `status` is an active account. */
|
|
13
|
+
export declare const ABSENT_USER_ACCOUNT_STATUS: "active";
|
|
14
|
+
/** A stored `userProfiles.status` read as the account's status: absent is
|
|
15
|
+
* `ABSENT_USER_ACCOUNT_STATUS`; any value outside `UserAccountStatusSchema` throws, so a
|
|
16
|
+
* malformed status is never enforced, compared, or shown as if it were real. */
|
|
17
|
+
export declare function parseUserAccountStatus(value: unknown): UserAccountStatus;
|
|
11
18
|
export declare const CraftSkillKindSchema: z.ZodEnum<{
|
|
12
19
|
original: "original";
|
|
13
20
|
mimicOnTtt: "mimicOnTtt";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/user.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgBxB;;yDAEyD;AACzD,eAAO,MAAM,uBAAuB;;;;EAA4C,CAAC;AACjF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/user.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgBxB;;yDAEyD;AACzD,eAAO,MAAM,uBAAuB;;;;EAA4C,CAAC;AACjF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;oDACoD;AACpD,eAAO,MAAM,0BAA0B,UAAgD,CAAC;AAExF;;iFAEiF;AACjF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAExE;AAOD,eAAO,MAAM,oBAAoB;;;;EAAoD,CAAC;AACtF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;0EAE0E;AAC1E,eAAO,MAAM,iCAAiC,EAAE,cAA6B,CAAC;AAM9E,eAAO,MAAM,+BAA+B;;;;;;kBAKjC,CAAC;AACZ,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAKxF,eAAO,MAAM,2BAA2B;;;;;;;;kBAM7B,CAAC;AACZ,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAqBhF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAe3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmBpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,eAAO,MAAM,uBAAuB;;;;;;;;;iBAKlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,sBAAsB;;;;;;;;;;;iBAOjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,2BAA2B;;;;;;iBAMtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwCzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD,eAAO,MAAM,oBAAoB;;;;;;;;;iBAqB/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uBAAuB;;;;;iBAalC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8DhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
package/dist/doc-schemas/user.js
CHANGED
|
@@ -18,6 +18,15 @@ const mediaKindSchema = ContentMediaKindSchema;
|
|
|
18
18
|
* authoritative value; the chat access projection and the status-reconcile queue
|
|
19
19
|
* derive from the same set). Never re-declare inline. */
|
|
20
20
|
export const UserAccountStatusSchema = z.enum(['active', 'suspended', 'banned']);
|
|
21
|
+
/** The status an account holds while no status decision has ever been written: a profile, or a
|
|
22
|
+
* token, with no `status` is an active account. */
|
|
23
|
+
export const ABSENT_USER_ACCOUNT_STATUS = 'active';
|
|
24
|
+
/** A stored `userProfiles.status` read as the account's status: absent is
|
|
25
|
+
* `ABSENT_USER_ACCOUNT_STATUS`; any value outside `UserAccountStatusSchema` throws, so a
|
|
26
|
+
* malformed status is never enforced, compared, or shown as if it were real. */
|
|
27
|
+
export function parseUserAccountStatus(value) {
|
|
28
|
+
return value === undefined ? ABSENT_USER_ACCOUNT_STATUS : UserAccountStatusSchema.parse(value);
|
|
29
|
+
}
|
|
21
30
|
// Craft-skill kind — mandatory choice at upload, no default (see ttt-prod
|
|
22
31
|
// docs/design/profile-and-craft-skills.md). 'original' = the artisan's own original creation;
|
|
23
32
|
// 'mimicOnTtt' = a performance/recreation of work that originated in the TTT community
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/doc-schemas/user.ts"],"names":[],"mappings":"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","sourcesContent":["// User-related Firestore document SCHEMAS — the single source of truth for the\n// `userProfiles/{uid}` doc, its `privateData/{uid}` subdoc, and craft-skill shapes.\n// (The `publicUsers/{uid}` mirror lives in ./publicUser.) TypeScript types are\n// inferred from these schemas via `z.infer`, so a document's shape and its type\n// cannot drift apart. See docs/design/firestore-schema-registry.md (ttt-prod).\n\nimport { z } from 'zod';\nimport { HALL_WING_TYPE_KEYS } from '../types/content.js';\nimport { MAX_ARTISAN_LOCATION_LENGTH } from '../constants/business.js';\nimport { CRAFT_SKILL_TAG_VALUES } from '../constants/options.js';\nimport { userPrivateDataAgeFieldsShape } from './safety/age.js';\nimport { ContentMediaKindSchema } from './media-assets.js';\nimport { ModerationHiddenBySchema } from './moderation.js';\nimport {\n calendarDateSchema,\n documentIdSegmentSchema,\n} from '../schemas/atoms.js';\nimport { PublicDocumentAcceptanceSchema, PublicDocumentVersionOrNoneSchema } from './public-documents.js';\n\n// The canonical stored media kind — declared once in doc-schemas/media-assets.ts.\nconst mediaKindSchema = ContentMediaKindSchema;\n\n/** The ONE canonical application account status (userProfiles.status is the\n * authoritative value; the chat access projection and the status-reconcile queue\n * derive from the same set). Never re-declare inline. */\nexport const UserAccountStatusSchema = z.enum(['active', 'suspended', 'banned']);\nexport type UserAccountStatus = z.infer<typeof UserAccountStatusSchema>;\n\n// Craft-skill kind — mandatory choice at upload, no default (see ttt-prod\n// docs/design/profile-and-craft-skills.md). 'original' = the artisan's own original creation;\n// 'mimicOnTtt' = a performance/recreation of work that originated in the TTT community\n// (requires a source reference); 'mimicOffTtt' = a performance/recreation of work from\n// outside TTT (covers covers + public-domain works, no source reference).\nexport const CraftSkillKindSchema = z.enum(['original', 'mimicOnTtt', 'mimicOffTtt']);\nexport type CraftSkillKind = z.infer<typeof CraftSkillKindSchema>;\n\n/** The ONE craft-skill kind that carries a `source` reference. The discriminated\n * CraftSkillSchema arm below and every upload-time validator derive from this named\n * declaration rather than re-quoting the member (ENG-005 / ARCH-102). */\nexport const CRAFT_SKILL_KIND_REQUIRING_SOURCE: CraftSkillKind = 'mimicOnTtt';\n\n// Source reference for a 'mimicOnTtt' skill — the original artisan OR the original work,\n// linked mention-style (id only, no name snapshots, resolved at render per the Display\n// Identity Invariant). 'workProject' is the safe framing today; linking a specific hall\n// item is optional future scope.\nexport const CraftSkillSourceReferenceSchema = z\n .object({\n type: z.enum(['user', 'workProject']),\n id: documentIdSegmentSchema,\n })\n .strict();\nexport type CraftSkillSourceReference = z.infer<typeof CraftSkillSourceReferenceSchema>;\n\n// The agreed attestation recorded on the doc: which statement (keyed by kind) the\n// uploader agreed to, its version, and when (epoch ms, always a number — never a\n// Firestore Timestamp).\nexport const CraftSkillAttestationSchema = z\n .object({\n statementKind: CraftSkillKindSchema,\n statementVersion: z.number().int().min(1),\n agreedAt: z.number(),\n })\n .strict();\nexport type CraftSkillAttestation = z.infer<typeof CraftSkillAttestationSchema>;\n\n// Base craft-skill fields shared across all kinds. The `kind` + `source` relationship is\n// layered on top as a discriminated union so an invalid combination (e.g. a source on an\n// 'original', or a missing source on a 'mimicOnTtt') fails schema validation.\nconst craftSkillBaseShape = {\n id: z.string(),\n name: z.string(),\n mediaAssetId: z.string(),\n // Canonical discipline-tag allowlist — never open strings (DJ ruling 2026-07-13).\n tags: z.array(z.enum(CRAFT_SKILL_TAG_VALUES)),\n createdAt: z.number(),\n type: mediaKindSchema,\n // Moderation visibility flag. Absent/false = visible; true = hidden by the\n // craft-skill hide cascade (report auto-hide or admin action). Mirrored onto\n // every taggedCraftSkills index doc so discovery surfaces can filter it.\n hidden: z.boolean(),\n hiddenBy: ModerationHiddenBySchema.optional(),\n attestation: CraftSkillAttestationSchema,\n};\n\nexport const CraftSkillSchema = z.discriminatedUnion('kind', [\n z.object({\n ...craftSkillBaseShape,\n kind: z.literal('original'),\n }),\n z.object({\n ...craftSkillBaseShape,\n kind: z.literal('mimicOnTtt'),\n // REQUIRED for the on-TTT mimic kind: the linked original artisan or work.\n source: CraftSkillSourceReferenceSchema,\n }),\n z.object({\n ...craftSkillBaseShape,\n kind: z.literal('mimicOffTtt'),\n }),\n]);\nexport type CraftSkill = z.infer<typeof CraftSkillSchema>;\n\nexport const CraftSkillReferenceSchema = z.object({\n craftSkillId: z.string(),\n compositeId: z.string(),\n userId: z.string(),\n craftSkillName: z.string(),\n craftSkillAssetId: z.string(),\n craftSkillType: mediaKindSchema,\n tags: z.array(z.enum(CRAFT_SKILL_TAG_VALUES)),\n createdAt: z.number(),\n // Mirror of CraftSkill.hidden; lets the tag-browse filter hidden skills out.\n hidden: z.boolean(),\n hiddenBy: ModerationHiddenBySchema.optional(),\n // Mirror of CraftSkill.kind so the tag-browse card can render the kind badge without a\n // second read.\n kind: CraftSkillKindSchema,\n // Mirror of CraftSkill.source — present ONLY when kind === 'mimicOnTtt' (the source-doc\n // discriminated union guarantees that invariant on the authoritative side). Lets the\n // browse card render the clickable \"someone performed my work\" source link.\n source: CraftSkillSourceReferenceSchema.optional(),\n});\nexport type CraftSkillReference = z.infer<typeof CraftSkillReferenceSchema>;\n\nexport const MinimalCraftSkillSchema = z.object({\n id: z.string(),\n name: z.string(),\n mediaAssetId: z.string(),\n type: mediaKindSchema,\n});\nexport type MinimalCraftSkill = z.infer<typeof MinimalCraftSkillSchema>;\n\nexport const OwnedWorkProjectSchema = z.object({\n workProjectId: z.string(),\n workingTitle: z.string(),\n workingDescription: z.string(),\n type: z.string(),\n createdOn: z.number(),\n hallWingType: z.enum(HALL_WING_TYPE_KEYS),\n});\nexport type OwnedWorkProject = z.infer<typeof OwnedWorkProjectSchema>;\n\nexport const AssociatedWorkProjectSchema = z.object({\n workProjectId: z.string(),\n workingTitle: z.string(),\n workingDescription: z.string(),\n type: z.string(),\n joinedOn: z.number(),\n});\nexport type AssociatedWorkProject = z.infer<typeof AssociatedWorkProjectSchema>;\n\nexport const FullUserSchema = z.object({\n uid: z.string(),\n displayName: z.string(),\n displayName_lowercase: z.string(),\n // One asset, variants full/medium/small in the mediaAssets registry; URLs\n // are built at render time (media-assets-and-protected-serving.md).\n profilePictureAssetId: z.string().nullable().optional(),\n artisanCreator: z.number().optional(),\n // Cosmetic supporter badge. Set true via the Admin SDK inside the Stripe webhook's\n // pledge-record transaction once the user completes a pledge; rendered on the profile\n // page. Lives here (not publicUsers) because userProfiles/{uid} is already readable by\n // any authenticated user, so other viewers see the badge with no mirror. Not PII.\n // Revoked (→ false) on refund once refunds ship.\n hasPledged: z.boolean().optional(),\n status: UserAccountStatusSchema.optional(),\n // Stamped by the one status writer on every status change: when, and the acting admin/operator\n // uid (a system actor writes its own id). Absent on an account whose status never changed.\n statusUpdatedAt: z.number().optional(),\n // Either the authenticated actor's uid or a stable namespaced system actor\n // identifier (for example `system:autoHashLock`); never null.\n statusUpdatedBy: z.string().min(1).optional(),\n // Chat-edge-rebuild account-access domain (Contract B / round-10 blocker 1): the\n // single ban/unban ordering version + state the chat `accountAccess` sync events key\n // on. Backend-only-writable. Deliberate defaults when ABSENT: a never-touched account\n // is `{ accountAccessVersion: 0, accountAccessState: 'active' }` (NOT banned) — bumped\n // atomically on suspend/ban/unban, which enqueue the accountAccessChanged fanout.\n accountAccessVersion: z.number().optional(),\n accountAccessState: UserAccountStatusSchema.optional(),\n // Moderation: set true by the forceDisplayNameReset callable when an admin resets an\n // abusive display name. The app gates the user into a forced \"pick a new name\" flow until\n // they complete it via setMyDisplayName (which clears this). Backend-only-writable (like `status`).\n displayNameResetRequired: z.boolean().optional(),\n ownedWorkProjects: z.array(OwnedWorkProjectSchema).optional(),\n associatedWorkProjects: z.array(AssociatedWorkProjectSchema).optional(),\n createdAt: z.number(),\n // N3 data-deletion / GDPR erasure: epoch ms when the account-deletion scrub\n // anonymized this profile in place. When set, the identity fields have been\n // scrubbed (displayName → the FORMER_MEMBER_DISPLAY_NAME sentinel) and the\n // account is a tombstone. Backend-only-writable.\n anonymizedAt: z.number().optional(),\n});\nexport type FullUser = z.infer<typeof FullUserSchema>;\n\nexport const UserAgreementsSchema = z.object({\n age: z.boolean().optional(),\n nudity: z.boolean().optional(),\n meet: z.boolean().optional(),\n cookies: z.boolean().optional(),\n terms: z.boolean().optional(),\n agreedOn: z.number().optional(),\n /**\n * The Terms / Privacy versions accepted at registration, read server-side from the live\n * docs at acceptance time — never client-supplied, never a hardcoded constant. Kept for the\n * accounts that recorded them; the current per-document record is\n * `privateData.publicDocumentAcceptance`.\n *\n * `0` is the sentinel for \"registered while no legal document was published\" (a fresh\n * environment's first account). Published versions start at 1, so 0 can never equal a live\n * version — which is deliberate: it keeps the record from claiming consent that never\n * happened, and leaves the version-bump re-acceptance path free to catch the account once\n * real legal content ships.\n */\n termsVersion: z.number().int().nonnegative().optional(),\n privacyVersion: z.number().int().nonnegative().optional(),\n});\nexport type UserAgreements = z.infer<typeof UserAgreementsSchema>;\n\n/**\n * First-visit site-tour state (account-durable, server-written) stored on\n * `privateData/{uid}.siteTour`. A missing `siteTour` field means the member has never\n * handled the tour. Automatic-invitation eligibility is evaluated in this order:\n * 1. `automaticInvitesDisabledAt` present → never invite automatically (\"Don't show\n * this again\");\n * 2. `notTodayDate` equals the member's current LOCAL calendar date → do not invite\n * that day (\"Not today\");\n * 3. `completedVersion` equals `SITE_TOUR_CURRENT_VERSION` → already handled;\n * 4. otherwise → offer the tour.\n * Written ONLY by the `updateSiteTourPreference` callable via the Admin SDK; never\n * client-written. `notTodayDate` is a strict `YYYY-MM-DD` local-date string (same\n * date-string convention as the age cluster's `attestedDateOfBirth`). See\n * docs/design/landing-backstage-guide-and-first-visit-plan.md §10.\n */\nexport const UserSiteTourStateSchema = z.object({\n // The tour version the member completed. The callable stamps SITE_TOUR_CURRENT_VERSION\n // server-side at completion (the version is server-owned, never client-supplied);\n // absent until a completion is recorded.\n completedVersion: z.number().int().optional(),\n // Epoch ms when the tour was completed.\n completedAt: z.number().optional(),\n // The member's LOCAL calendar date (YYYY-MM-DD) recorded by \"Not today\"; the automatic\n // invitation is suppressed for exactly that date, then offered again on a later date.\n notTodayDate: calendarDateSchema.optional(), // 'YYYY-MM-DD'\n // Epoch ms when the member chose \"Don't show this again\" (permanent automatic-invite\n // dismissal). Manual replay from Help ignores this without clearing it.\n automaticInvitesDisabledAt: z.number().optional(),\n});\nexport type UserSiteTourState = z.infer<typeof UserSiteTourStateSchema>;\n\n/**\n * Owner-only account state at `userProfiles/{uid}/privateData/{uid}` (readable\n * only by the owner; never mirrored to publicUsers).\n */\nexport const UserPrivateDataSchema = z.object({\n email: z.string(),\n // Epoch ms when the user applied for news/political posting (not a boolean) so the\n // applicant queue can be ordered/triaged by application date. Mirrors the\n // `artisanCreator?: number` grant-timestamp pattern. Truthy when set, so existing\n // truthiness reads keep working; absent until the user applies.\n isWaitingForNewsApproval: z.number().optional(),\n // Epoch ms when the user attested they are a U.S.-based person while becoming an artisan\n // (required at artisan signup for now). Mirrors the ageAttested attestation pattern; written\n // server-side by becomeArtisanCreator.\n usPersonAttestedAt: z.number().optional(),\n // Non-U.S. artisan-interest \"waitlist\" recorded ONLY for adult accounts when a non-U.S. person\n // asks to be notified once creator signups open in their region. Owner-only, backend-written.\n // `requestedAt` is the immutable first-come-first-serve timestamp; `country`/`region` let signups\n // be opened by jurisdiction as each region's laws clear. Mirrors isWaitingForNewsApproval.\n nonUsArtisanInterest: z\n .object({\n country: z.string().min(1).max(MAX_ARTISAN_LOCATION_LENGTH),\n region: z.string().max(MAX_ARTISAN_LOCATION_LENGTH).optional(),\n requestedAt: z.number(),\n })\n .optional(),\n squareStreetzAgreementsDate: z.number().optional(),\n // The rules-and-agreements `version` in force when the user accepted; 0 when no versioned\n // rules doc existed yet, so a later real version 1 still re-prompts.\n squareStreetzAgreementsVersion: PublicDocumentVersionOrNoneSchema.optional(),\n // The rules-and-agreements version in force when the user accepted the Artisan upgrade's\n // agreement, read server-side in the grant transaction (the grant time is the profile's\n // `artisanCreator` stamp). Same 0 sentinel as `squareStreetzAgreementsVersion`.\n artisanCreatorAgreementsVersion: PublicDocumentVersionOrNoneSchema.optional(),\n // Epoch ms when the user accepted the one-time Hall download acknowledgement\n // (personal offline use only, no redistribution). Written server-side by the\n // acceptHallDownloadAcknowledgement callable; gates the Hall download button.\n hallDownloadAcknowledgedAt: z.number().optional(),\n agreements: UserAgreementsSchema.optional(),\n // The latest accepted version of each public document and the accepted level (see\n // PublicDocumentAcceptanceSchema). Written by registration and the acceptance callable only;\n // the `docsAccepted` claim mirrors `acceptedLevel`. Absent = nothing accepted (level 0).\n publicDocumentAcceptance: PublicDocumentAcceptanceSchema.optional(),\n // First-visit site-tour state (account-durable; the SOLE authority for tour\n // eligibility — no local-storage cache/mirror). Absent = never handled. Written only\n // by the updateSiteTourPreference callable. See UserSiteTourStateSchema above.\n siteTour: UserSiteTourStateSchema.optional(),\n // Moderation: human-readable reason for the current account status (set when an\n // admin suspends/bans the user). Written by the setUserStatus callable via the\n // Admin SDK; readable only by the owner (shown in their restricted view).\n // Cleared when the account is reinstated to 'active'.\n statusReason: z.string().optional(),\n statusReasonAt: z.number().optional(),\n // Trust & Safety (§A8): silent interim safety-lock flag. Backend-only-writable,\n // restricted to the owner's view. Set true while a child-safety/NCII action is\n // pending against the account; the app gates the user accordingly.\n safetyLocked: z.boolean().optional(),\n // Counts the owner's Craft publishes and deletes. Each one reads this doc and writes the next\n // value inside its own transaction, so two publishes that both counted a free Craft slot write\n // the same document and conflict instead of both landing past the cap (BACKEND-116). Absent\n // until the first Craft change (read as 0); a moderation hide never writes it.\n craftSkillChangeMarker: z.number().int().nonnegative().optional(),\n // Trust & Safety (§A7): server-write-only age/registration fields, merged from the\n // standalone age cluster shape (./safety/age.js) so the canonical schema and the\n // age cluster cannot drift.\n ...userPrivateDataAgeFieldsShape,\n});\nexport type UserPrivateData = z.infer<typeof UserPrivateDataSchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/doc-schemas/user.ts"],"names":[],"mappings":"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","sourcesContent":["// User-related Firestore document SCHEMAS — the single source of truth for the\n// `userProfiles/{uid}` doc, its `privateData/{uid}` subdoc, and craft-skill shapes.\n// (The `publicUsers/{uid}` mirror lives in ./publicUser.) TypeScript types are\n// inferred from these schemas via `z.infer`, so a document's shape and its type\n// cannot drift apart. See docs/design/firestore-schema-registry.md (ttt-prod).\n\nimport { z } from 'zod';\nimport { HALL_WING_TYPE_KEYS } from '../types/content.js';\nimport { MAX_ARTISAN_LOCATION_LENGTH } from '../constants/business.js';\nimport { CRAFT_SKILL_TAG_VALUES } from '../constants/options.js';\nimport { userPrivateDataAgeFieldsShape } from './safety/age.js';\nimport { ContentMediaKindSchema } from './media-assets.js';\nimport { ModerationHiddenBySchema } from './moderation.js';\nimport {\n calendarDateSchema,\n documentIdSegmentSchema,\n} from '../schemas/atoms.js';\nimport { PublicDocumentAcceptanceSchema, PublicDocumentVersionOrNoneSchema } from './public-documents.js';\n\n// The canonical stored media kind — declared once in doc-schemas/media-assets.ts.\nconst mediaKindSchema = ContentMediaKindSchema;\n\n/** The ONE canonical application account status (userProfiles.status is the\n * authoritative value; the chat access projection and the status-reconcile queue\n * derive from the same set). Never re-declare inline. */\nexport const UserAccountStatusSchema = z.enum(['active', 'suspended', 'banned']);\nexport type UserAccountStatus = z.infer<typeof UserAccountStatusSchema>;\n\n/** The status an account holds while no status decision has ever been written: a profile, or a\n * token, with no `status` is an active account. */\nexport const ABSENT_USER_ACCOUNT_STATUS = 'active' as const satisfies UserAccountStatus;\n\n/** A stored `userProfiles.status` read as the account's status: absent is\n * `ABSENT_USER_ACCOUNT_STATUS`; any value outside `UserAccountStatusSchema` throws, so a\n * malformed status is never enforced, compared, or shown as if it were real. */\nexport function parseUserAccountStatus(value: unknown): UserAccountStatus {\n return value === undefined ? ABSENT_USER_ACCOUNT_STATUS : UserAccountStatusSchema.parse(value);\n}\n\n// Craft-skill kind — mandatory choice at upload, no default (see ttt-prod\n// docs/design/profile-and-craft-skills.md). 'original' = the artisan's own original creation;\n// 'mimicOnTtt' = a performance/recreation of work that originated in the TTT community\n// (requires a source reference); 'mimicOffTtt' = a performance/recreation of work from\n// outside TTT (covers covers + public-domain works, no source reference).\nexport const CraftSkillKindSchema = z.enum(['original', 'mimicOnTtt', 'mimicOffTtt']);\nexport type CraftSkillKind = z.infer<typeof CraftSkillKindSchema>;\n\n/** The ONE craft-skill kind that carries a `source` reference. The discriminated\n * CraftSkillSchema arm below and every upload-time validator derive from this named\n * declaration rather than re-quoting the member (ENG-005 / ARCH-102). */\nexport const CRAFT_SKILL_KIND_REQUIRING_SOURCE: CraftSkillKind = 'mimicOnTtt';\n\n// Source reference for a 'mimicOnTtt' skill — the original artisan OR the original work,\n// linked mention-style (id only, no name snapshots, resolved at render per the Display\n// Identity Invariant). 'workProject' is the safe framing today; linking a specific hall\n// item is optional future scope.\nexport const CraftSkillSourceReferenceSchema = z\n .object({\n type: z.enum(['user', 'workProject']),\n id: documentIdSegmentSchema,\n })\n .strict();\nexport type CraftSkillSourceReference = z.infer<typeof CraftSkillSourceReferenceSchema>;\n\n// The agreed attestation recorded on the doc: which statement (keyed by kind) the\n// uploader agreed to, its version, and when (epoch ms, always a number — never a\n// Firestore Timestamp).\nexport const CraftSkillAttestationSchema = z\n .object({\n statementKind: CraftSkillKindSchema,\n statementVersion: z.number().int().min(1),\n agreedAt: z.number(),\n })\n .strict();\nexport type CraftSkillAttestation = z.infer<typeof CraftSkillAttestationSchema>;\n\n// Base craft-skill fields shared across all kinds. The `kind` + `source` relationship is\n// layered on top as a discriminated union so an invalid combination (e.g. a source on an\n// 'original', or a missing source on a 'mimicOnTtt') fails schema validation.\nconst craftSkillBaseShape = {\n id: z.string(),\n name: z.string(),\n mediaAssetId: z.string(),\n // Canonical discipline-tag allowlist — never open strings (DJ ruling 2026-07-13).\n tags: z.array(z.enum(CRAFT_SKILL_TAG_VALUES)),\n createdAt: z.number(),\n type: mediaKindSchema,\n // Moderation visibility flag. Absent/false = visible; true = hidden by the\n // craft-skill hide cascade (report auto-hide or admin action). Mirrored onto\n // every taggedCraftSkills index doc so discovery surfaces can filter it.\n hidden: z.boolean(),\n hiddenBy: ModerationHiddenBySchema.optional(),\n attestation: CraftSkillAttestationSchema,\n};\n\nexport const CraftSkillSchema = z.discriminatedUnion('kind', [\n z.object({\n ...craftSkillBaseShape,\n kind: z.literal('original'),\n }),\n z.object({\n ...craftSkillBaseShape,\n kind: z.literal('mimicOnTtt'),\n // REQUIRED for the on-TTT mimic kind: the linked original artisan or work.\n source: CraftSkillSourceReferenceSchema,\n }),\n z.object({\n ...craftSkillBaseShape,\n kind: z.literal('mimicOffTtt'),\n }),\n]);\nexport type CraftSkill = z.infer<typeof CraftSkillSchema>;\n\nexport const CraftSkillReferenceSchema = z.object({\n craftSkillId: z.string(),\n compositeId: z.string(),\n userId: z.string(),\n craftSkillName: z.string(),\n craftSkillAssetId: z.string(),\n craftSkillType: mediaKindSchema,\n tags: z.array(z.enum(CRAFT_SKILL_TAG_VALUES)),\n createdAt: z.number(),\n // Mirror of CraftSkill.hidden; lets the tag-browse filter hidden skills out.\n hidden: z.boolean(),\n hiddenBy: ModerationHiddenBySchema.optional(),\n // Mirror of CraftSkill.kind so the tag-browse card can render the kind badge without a\n // second read.\n kind: CraftSkillKindSchema,\n // Mirror of CraftSkill.source — present ONLY when kind === 'mimicOnTtt' (the source-doc\n // discriminated union guarantees that invariant on the authoritative side). Lets the\n // browse card render the clickable \"someone performed my work\" source link.\n source: CraftSkillSourceReferenceSchema.optional(),\n});\nexport type CraftSkillReference = z.infer<typeof CraftSkillReferenceSchema>;\n\nexport const MinimalCraftSkillSchema = z.object({\n id: z.string(),\n name: z.string(),\n mediaAssetId: z.string(),\n type: mediaKindSchema,\n});\nexport type MinimalCraftSkill = z.infer<typeof MinimalCraftSkillSchema>;\n\nexport const OwnedWorkProjectSchema = z.object({\n workProjectId: z.string(),\n workingTitle: z.string(),\n workingDescription: z.string(),\n type: z.string(),\n createdOn: z.number(),\n hallWingType: z.enum(HALL_WING_TYPE_KEYS),\n});\nexport type OwnedWorkProject = z.infer<typeof OwnedWorkProjectSchema>;\n\nexport const AssociatedWorkProjectSchema = z.object({\n workProjectId: z.string(),\n workingTitle: z.string(),\n workingDescription: z.string(),\n type: z.string(),\n joinedOn: z.number(),\n});\nexport type AssociatedWorkProject = z.infer<typeof AssociatedWorkProjectSchema>;\n\nexport const FullUserSchema = z.object({\n uid: z.string(),\n displayName: z.string(),\n displayName_lowercase: z.string(),\n // One asset, variants full/medium/small in the mediaAssets registry; URLs\n // are built at render time (media-assets-and-protected-serving.md).\n profilePictureAssetId: z.string().nullable().optional(),\n artisanCreator: z.number().optional(),\n // Cosmetic supporter badge. Set true via the Admin SDK inside the Stripe webhook's\n // pledge-record transaction once the user completes a pledge; rendered on the profile\n // page. Lives here (not publicUsers) because userProfiles/{uid} is already readable by\n // any authenticated user, so other viewers see the badge with no mirror. Not PII.\n // Revoked (→ false) on refund once refunds ship.\n hasPledged: z.boolean().optional(),\n status: UserAccountStatusSchema.optional(),\n // Stamped by the one status writer on every status change: when, and the acting admin/operator\n // uid (a system actor writes its own id). Absent on an account whose status never changed.\n statusUpdatedAt: z.number().optional(),\n // Either the authenticated actor's uid or a stable namespaced system actor\n // identifier (for example `system:autoHashLock`); never null.\n statusUpdatedBy: z.string().min(1).optional(),\n // Chat-edge-rebuild account-access domain (Contract B / round-10 blocker 1): the\n // single ban/unban ordering version + state the chat `accountAccess` sync events key\n // on. Backend-only-writable. Deliberate defaults when ABSENT: a never-touched account\n // is `{ accountAccessVersion: 0, accountAccessState: 'active' }` (NOT banned) — bumped\n // atomically on suspend/ban/unban, which enqueue the accountAccessChanged fanout.\n accountAccessVersion: z.number().optional(),\n accountAccessState: UserAccountStatusSchema.optional(),\n // Moderation: set true by the forceDisplayNameReset callable when an admin resets an\n // abusive display name. The app gates the user into a forced \"pick a new name\" flow until\n // they complete it via setMyDisplayName (which clears this). Backend-only-writable (like `status`).\n displayNameResetRequired: z.boolean().optional(),\n ownedWorkProjects: z.array(OwnedWorkProjectSchema).optional(),\n associatedWorkProjects: z.array(AssociatedWorkProjectSchema).optional(),\n createdAt: z.number(),\n // N3 data-deletion / GDPR erasure: epoch ms when the account-deletion scrub\n // anonymized this profile in place. When set, the identity fields have been\n // scrubbed (displayName → the FORMER_MEMBER_DISPLAY_NAME sentinel) and the\n // account is a tombstone. Backend-only-writable.\n anonymizedAt: z.number().optional(),\n});\nexport type FullUser = z.infer<typeof FullUserSchema>;\n\nexport const UserAgreementsSchema = z.object({\n age: z.boolean().optional(),\n nudity: z.boolean().optional(),\n meet: z.boolean().optional(),\n cookies: z.boolean().optional(),\n terms: z.boolean().optional(),\n agreedOn: z.number().optional(),\n /**\n * The Terms / Privacy versions accepted at registration, read server-side from the live\n * docs at acceptance time — never client-supplied, never a hardcoded constant. Kept for the\n * accounts that recorded them; the current per-document record is\n * `privateData.publicDocumentAcceptance`.\n *\n * `0` is the sentinel for \"registered while no legal document was published\" (a fresh\n * environment's first account). Published versions start at 1, so 0 can never equal a live\n * version — which is deliberate: it keeps the record from claiming consent that never\n * happened, and leaves the version-bump re-acceptance path free to catch the account once\n * real legal content ships.\n */\n termsVersion: z.number().int().nonnegative().optional(),\n privacyVersion: z.number().int().nonnegative().optional(),\n});\nexport type UserAgreements = z.infer<typeof UserAgreementsSchema>;\n\n/**\n * First-visit site-tour state (account-durable, server-written) stored on\n * `privateData/{uid}.siteTour`. A missing `siteTour` field means the member has never\n * handled the tour. Automatic-invitation eligibility is evaluated in this order:\n * 1. `automaticInvitesDisabledAt` present → never invite automatically (\"Don't show\n * this again\");\n * 2. `notTodayDate` equals the member's current LOCAL calendar date → do not invite\n * that day (\"Not today\");\n * 3. `completedVersion` equals `SITE_TOUR_CURRENT_VERSION` → already handled;\n * 4. otherwise → offer the tour.\n * Written ONLY by the `updateSiteTourPreference` callable via the Admin SDK; never\n * client-written. `notTodayDate` is a strict `YYYY-MM-DD` local-date string (same\n * date-string convention as the age cluster's `attestedDateOfBirth`). See\n * docs/design/landing-backstage-guide-and-first-visit-plan.md §10.\n */\nexport const UserSiteTourStateSchema = z.object({\n // The tour version the member completed. The callable stamps SITE_TOUR_CURRENT_VERSION\n // server-side at completion (the version is server-owned, never client-supplied);\n // absent until a completion is recorded.\n completedVersion: z.number().int().optional(),\n // Epoch ms when the tour was completed.\n completedAt: z.number().optional(),\n // The member's LOCAL calendar date (YYYY-MM-DD) recorded by \"Not today\"; the automatic\n // invitation is suppressed for exactly that date, then offered again on a later date.\n notTodayDate: calendarDateSchema.optional(), // 'YYYY-MM-DD'\n // Epoch ms when the member chose \"Don't show this again\" (permanent automatic-invite\n // dismissal). Manual replay from Help ignores this without clearing it.\n automaticInvitesDisabledAt: z.number().optional(),\n});\nexport type UserSiteTourState = z.infer<typeof UserSiteTourStateSchema>;\n\n/**\n * Owner-only account state at `userProfiles/{uid}/privateData/{uid}` (readable\n * only by the owner; never mirrored to publicUsers).\n */\nexport const UserPrivateDataSchema = z.object({\n email: z.string(),\n // Epoch ms when the user applied for news/political posting (not a boolean) so the\n // applicant queue can be ordered/triaged by application date. Mirrors the\n // `artisanCreator?: number` grant-timestamp pattern. Truthy when set, so existing\n // truthiness reads keep working; absent until the user applies.\n isWaitingForNewsApproval: z.number().optional(),\n // Epoch ms when the user attested they are a U.S.-based person while becoming an artisan\n // (required at artisan signup for now). Mirrors the ageAttested attestation pattern; written\n // server-side by becomeArtisanCreator.\n usPersonAttestedAt: z.number().optional(),\n // Non-U.S. artisan-interest \"waitlist\" recorded ONLY for adult accounts when a non-U.S. person\n // asks to be notified once creator signups open in their region. Owner-only, backend-written.\n // `requestedAt` is the immutable first-come-first-serve timestamp; `country`/`region` let signups\n // be opened by jurisdiction as each region's laws clear. Mirrors isWaitingForNewsApproval.\n nonUsArtisanInterest: z\n .object({\n country: z.string().min(1).max(MAX_ARTISAN_LOCATION_LENGTH),\n region: z.string().max(MAX_ARTISAN_LOCATION_LENGTH).optional(),\n requestedAt: z.number(),\n })\n .optional(),\n squareStreetzAgreementsDate: z.number().optional(),\n // The rules-and-agreements `version` in force when the user accepted; 0 when no versioned\n // rules doc existed yet, so a later real version 1 still re-prompts.\n squareStreetzAgreementsVersion: PublicDocumentVersionOrNoneSchema.optional(),\n // The rules-and-agreements version in force when the user accepted the Artisan upgrade's\n // agreement, read server-side in the grant transaction (the grant time is the profile's\n // `artisanCreator` stamp). Same 0 sentinel as `squareStreetzAgreementsVersion`.\n artisanCreatorAgreementsVersion: PublicDocumentVersionOrNoneSchema.optional(),\n // Epoch ms when the user accepted the one-time Hall download acknowledgement\n // (personal offline use only, no redistribution). Written server-side by the\n // acceptHallDownloadAcknowledgement callable; gates the Hall download button.\n hallDownloadAcknowledgedAt: z.number().optional(),\n agreements: UserAgreementsSchema.optional(),\n // The latest accepted version of each public document and the accepted level (see\n // PublicDocumentAcceptanceSchema). Written by registration and the acceptance callable only;\n // the `docsAccepted` claim mirrors `acceptedLevel`. Absent = nothing accepted (level 0).\n publicDocumentAcceptance: PublicDocumentAcceptanceSchema.optional(),\n // First-visit site-tour state (account-durable; the SOLE authority for tour\n // eligibility — no local-storage cache/mirror). Absent = never handled. Written only\n // by the updateSiteTourPreference callable. See UserSiteTourStateSchema above.\n siteTour: UserSiteTourStateSchema.optional(),\n // Moderation: human-readable reason for the current account status (set when an\n // admin suspends/bans the user). Written by the setUserStatus callable via the\n // Admin SDK; readable only by the owner (shown in their restricted view).\n // Cleared when the account is reinstated to 'active'.\n statusReason: z.string().optional(),\n statusReasonAt: z.number().optional(),\n // Trust & Safety (§A8): silent interim safety-lock flag. Backend-only-writable,\n // restricted to the owner's view. Set true while a child-safety/NCII action is\n // pending against the account; the app gates the user accordingly.\n safetyLocked: z.boolean().optional(),\n // Counts the owner's Craft publishes and deletes. Each one reads this doc and writes the next\n // value inside its own transaction, so two publishes that both counted a free Craft slot write\n // the same document and conflict instead of both landing past the cap (BACKEND-116). Absent\n // until the first Craft change (read as 0); a moderation hide never writes it.\n craftSkillChangeMarker: z.number().int().nonnegative().optional(),\n // Trust & Safety (§A7): server-write-only age/registration fields, merged from the\n // standalone age cluster shape (./safety/age.js) so the canonical schema and the\n // age cluster cannot drift.\n ...userPrivateDataAgeFieldsShape,\n});\nexport type UserPrivateData = z.infer<typeof UserPrivateDataSchema>;\n"]}
|
|
@@ -51,25 +51,38 @@ export interface AppConfigLeverRepair {
|
|
|
51
51
|
readonly lever: AppConfigLeverKey;
|
|
52
52
|
readonly storedValue: unknown;
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* The doc a writer writes, every stored lever the merge had to repair to write it, and whether the
|
|
56
|
+
* stored version block must be left as it is.
|
|
57
|
+
*/
|
|
55
58
|
export interface AppConfigMerge {
|
|
56
59
|
readonly next: AppConfig;
|
|
57
60
|
readonly repairs: readonly AppConfigLeverRepair[];
|
|
61
|
+
/**
|
|
62
|
+
* True when the stored version block does not parse and the update does not replace it. `next`
|
|
63
|
+
* then holds no block, and the writer must write `next` as a MERGE (never a full overwrite), so
|
|
64
|
+
* the stored block is kept as it is — the read side keeps reporting it `invalid` (failing closed
|
|
65
|
+
* and alerting) instead of it disappearing and reading as "nothing released".
|
|
66
|
+
*/
|
|
67
|
+
readonly keepsStoredVersionBlock: boolean;
|
|
58
68
|
}
|
|
59
69
|
/**
|
|
60
|
-
* The
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
70
|
+
* The `_appConfig/app` doc to write after applying `update` to `existing` (the doc as read in the
|
|
71
|
+
* writer's transaction; `undefined` or `null` when it does not exist). The update's fields win;
|
|
72
|
+
* `existing` supplies the rest; any field still missing comes from DEFAULT_APP_CONFIG. An
|
|
73
|
+
* `undefined` update field changes nothing. Pure.
|
|
64
74
|
*
|
|
65
|
-
* A stored
|
|
66
|
-
* maintenance kill switch:
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
75
|
+
* A stored value its schema refuses never blocks a save — a console typo must not disable the
|
|
76
|
+
* maintenance kill switch:
|
|
77
|
+
* - a refused LEVER is dropped, so the update's value replaces it when the update sends that lever
|
|
78
|
+
* and its DEFAULT_APP_CONFIG value (what every reader already falls back to) otherwise; it is
|
|
79
|
+
* reported in `repairs`;
|
|
80
|
+
* - a refused version block has no default and must never read as "nothing released", so when the
|
|
81
|
+
* update does not send a block it is left untouched (`keepsStoredVersionBlock`); an update that
|
|
82
|
+
* sends a block replaces it.
|
|
70
83
|
*
|
|
71
|
-
* Throws when the
|
|
72
|
-
*
|
|
84
|
+
* Throws when the update itself would not parse under AppConfigSchema, so nothing invalid is ever
|
|
85
|
+
* written.
|
|
73
86
|
*/
|
|
74
87
|
export declare function mergeAppConfigUpdate(existing: Readonly<Record<string, unknown>> | null | undefined, update: Readonly<Partial<AppConfig>>): AppConfigMerge;
|
|
75
88
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/utils/app-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/utils/app-config.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAErF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC;AAE5F,oEAAoE;AACpE,MAAM,MAAM,iBAAiB,GAAG,MAAM,eAAe,CAAC;AAEtD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,EAAE,eAO/B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,2BAA2B,mHACqE,CAAC;AAE9G;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAG,aAAsB,CAAC;AAE/D,qEAAqE;AACrE,eAAO,MAAM,0BAA0B,EAAG,aAAsB,CAAC;AAEjE,sEAAsE;AACtE,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,0BAA0B,CAAC;CACpD;AASD,2EAA2E;AAC3E,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,yBAAyB,CAGlG;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAM/D;AAMD,yFAAyF;AACzF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAClD;;;;;OAKG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAC9D,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,GACnC,cAAc,CAuBhB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,iBAAiB,EAC5D,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAC9D,KAAK,EAAE,CAAC,GACP,eAAe,CAAC,CAAC,CAAC,CAKpB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,GAC7D,eAAe,CAMjB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iCAAiC,GACzC;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GACxD;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,0BAA0B,CAAA;CAAE,GACxE;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAO3D;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,GAC7D,iCAAiC,CAQnC"}
|
package/dist/utils/app-config.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// wiped environment) creates a doc WITHOUT the fields AppConfigSchema requires. The write rule
|
|
7
7
|
// is `mergeAppConfigUpdate(existing, update)`: the writer's update on top of the doc it read,
|
|
8
8
|
// with any field that doc lacks — and any lever it holds at a value its schema refuses — taken
|
|
9
|
-
// from DEFAULT_APP_CONFIG, validated as a whole.
|
|
9
|
+
// from DEFAULT_APP_CONFIG, validated as a whole. A stored version block that does not parse is
|
|
10
|
+
// never written over and never dropped: the writer leaves it as it is (see AppConfigMerge).
|
|
10
11
|
//
|
|
11
12
|
// READ. The stored doc is untrusted — an operator can console-edit any field into any shape —
|
|
12
13
|
// and a config read must never take the product down by itself. The read rule is
|
|
@@ -78,19 +79,22 @@ function definedFields(fields) {
|
|
|
78
79
|
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
|
-
* The
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* The `_appConfig/app` doc to write after applying `update` to `existing` (the doc as read in the
|
|
83
|
+
* writer's transaction; `undefined` or `null` when it does not exist). The update's fields win;
|
|
84
|
+
* `existing` supplies the rest; any field still missing comes from DEFAULT_APP_CONFIG. An
|
|
85
|
+
* `undefined` update field changes nothing. Pure.
|
|
85
86
|
*
|
|
86
|
-
* A stored
|
|
87
|
-
* maintenance kill switch:
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* A stored value its schema refuses never blocks a save — a console typo must not disable the
|
|
88
|
+
* maintenance kill switch:
|
|
89
|
+
* - a refused LEVER is dropped, so the update's value replaces it when the update sends that lever
|
|
90
|
+
* and its DEFAULT_APP_CONFIG value (what every reader already falls back to) otherwise; it is
|
|
91
|
+
* reported in `repairs`;
|
|
92
|
+
* - a refused version block has no default and must never read as "nothing released", so when the
|
|
93
|
+
* update does not send a block it is left untouched (`keepsStoredVersionBlock`); an update that
|
|
94
|
+
* sends a block replaces it.
|
|
91
95
|
*
|
|
92
|
-
* Throws when the
|
|
93
|
-
*
|
|
96
|
+
* Throws when the update itself would not parse under AppConfigSchema, so nothing invalid is ever
|
|
97
|
+
* written.
|
|
94
98
|
*/
|
|
95
99
|
export function mergeAppConfigUpdate(existing, update) {
|
|
96
100
|
const stored = definedFields(existing ?? {});
|
|
@@ -101,17 +105,19 @@ export function mergeAppConfigUpdate(existing, update) {
|
|
|
101
105
|
repairs.push({ lever, storedValue: stored[lever] });
|
|
102
106
|
delete stored[lever];
|
|
103
107
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
const sent = definedFields(update);
|
|
109
|
+
const keepsStoredVersionBlock = 'publicDocumentVersions' in stored &&
|
|
110
|
+
!('publicDocumentVersions' in sent) &&
|
|
111
|
+
!AppConfigSchema.shape.publicDocumentVersions.safeParse(stored.publicDocumentVersions).success;
|
|
112
|
+
if (keepsStoredVersionBlock)
|
|
113
|
+
delete stored.publicDocumentVersions;
|
|
114
|
+
const parsed = AppConfigSchema.safeParse({ ...DEFAULT_APP_CONFIG, ...stored, ...sent });
|
|
109
115
|
if (!parsed.success) {
|
|
110
116
|
const issue = parsed.error.issues[0];
|
|
111
117
|
const field = issue?.path.join('.') || 'document';
|
|
112
118
|
throw new Error(`_appConfig/app would not match AppConfigSchema after this update (${field}: ${issue?.message ?? 'invalid'}). Nothing should be written.`);
|
|
113
119
|
}
|
|
114
|
-
return { next: parsed.data, repairs };
|
|
120
|
+
return { next: parsed.data, repairs, keepsStoredVersionBlock };
|
|
115
121
|
}
|
|
116
122
|
/**
|
|
117
123
|
* One operator lever off an unvalidated `_appConfig/app` snapshot (the raw stored doc; `null` or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.js","sourceRoot":"","sources":["../../src/utils/app-config.ts"],"names":[],"mappings":"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","sourcesContent":["// The `_appConfig/app` singleton at rest, and the ONE rule each for writing to it and reading it.\n//\n// WRITE. Several backend writers touch this doc (the admin App configuration card through\n// updateAppConfig, the public-document release publish, a seed), and each sends only the fields\n// it owns. Written as a bare merge, the first of them to run after the doc is gone (a fresh or\n// wiped environment) creates a doc WITHOUT the fields AppConfigSchema requires. The write rule\n// is `mergeAppConfigUpdate(existing, update)`: the writer's update on top of the doc it read,\n// with any field that doc lacks — and any lever it holds at a value its schema refuses — taken\n// from DEFAULT_APP_CONFIG, validated as a whole.\n//\n// READ. The stored doc is untrusted — an operator can console-edit any field into any shape —\n// and a config read must never take the product down by itself. The read rule is\n// `readAppConfigLever(snapshot, lever)`: the lever's stored value when its AppConfigSchema field\n// accepts it, otherwise its DEFAULT_APP_CONFIG value, one lever at a time so a bad field never\n// costs the good ones. `readAppConfigLevers` is every lever through it. The backend's cached\n// snapshot reader and the client shell both read through these. The release-owned version block\n// has no default to fall back to, so its reader, `readPublicDocumentVersionBlock`, reports what it\n// found instead — absent, valid, or invalid — and each consumer applies its own policy.\n\nimport { AppConfigSchema, type AppConfig } from '../doc-schemas/system.js';\nimport type { PublicDocumentVersionBlock } from '../doc-schemas/public-documents.js';\n\n/**\n * Every operator lever of `_appConfig/app`, each at a value its AppConfigSchema field accepts.\n * The release-owned `publicDocumentVersions` block is not a lever.\n */\nexport type AppConfigLevers = Readonly<Required<Omit<AppConfig, 'publicDocumentVersions'>>>;\n\n/** The name of one operator lever — a key of DEFAULT_APP_CONFIG. */\nexport type AppConfigLeverKey = keyof AppConfigLevers;\n\n/**\n * Every operator lever of `_appConfig/app` at rest — the doc a fresh environment behaves as if\n * it held, and the value each reader falls back to for a missing or unreadable field. The OPEN\n * posture: maintenance off, registration open, no banner, no throttle.\n *\n * `appVersion` is `''`: no version has been published. VersionGate skips a falsy version, so it\n * stays dormant — no reload for any browser, whatever version it stored before — until an\n * operator sets a real one through updateAppConfig (whose input refuses `''`). From then on the\n * gate compares as usual.\n *\n * The release-owned `publicDocumentVersions` block is not a lever and has no default: it is\n * absent until the first release.\n */\nexport const DEFAULT_APP_CONFIG: AppConfigLevers = Object.freeze({\n appVersion: '',\n maintenanceMode: false,\n maintenanceMessage: '',\n registrationEnabled: true,\n announcementMessage: '',\n rateLimitMultiplier: 1,\n});\n\n/**\n * The line shown while maintenance is on and the operator left `maintenanceMessage` blank — on\n * the maintenance takeover screen and in the callable refusal alike.\n */\nexport const DEFAULT_MAINTENANCE_MESSAGE =\n 'TTT Productions is down for maintenance right now. We will be back shortly — thank you for your patience.';\n\n/**\n * The code the callable maintenance refusal is thrown with. `unavailable` is also what a real\n * outage produces, so the code alone never identifies the refusal — its details do.\n */\nexport const MAINTENANCE_REFUSAL_CODE = 'unavailable' as const;\n\n/** The `details.reason` the callable maintenance refusal carries. */\nexport const MAINTENANCE_REFUSAL_REASON = 'maintenance' as const;\n\n/** Structured details carried by the callable maintenance refusal. */\nexport interface MaintenanceRefusalDetails {\n readonly reason: typeof MAINTENANCE_REFUSAL_REASON;\n}\n\n// The refusal code as the server (`unavailable`) and the Firebase client SDK\n// (`functions/unavailable`) spell it.\nconst MAINTENANCE_REFUSAL_CODES: ReadonlySet<string> = new Set([\n MAINTENANCE_REFUSAL_CODE,\n `functions/${MAINTENANCE_REFUSAL_CODE}`,\n]);\n\n/** True when `details` is the maintenance refusal's structured details. */\nexport function isMaintenanceRefusalDetails(details: unknown): details is MaintenanceRefusalDetails {\n if (!details || typeof details !== 'object') return false;\n return (details as Record<string, unknown>).reason === MAINTENANCE_REFUSAL_REASON;\n}\n\n/**\n * True when `err` is the callable maintenance refusal — an `unavailable` error in either\n * spelling whose details carry `reason: 'maintenance'`. Works on the server-side error and on\n * the client-side callable error alike. Decided by the code and the details, never the message\n * text, so a genuine outage is never mistaken for a designed refusal.\n */\nexport function isMaintenanceRefusalError(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n const e = err as { code?: unknown; details?: unknown };\n return (\n typeof e.code === 'string' && MAINTENANCE_REFUSAL_CODES.has(e.code) && isMaintenanceRefusalDetails(e.details)\n );\n}\n\nfunction definedFields(fields: Readonly<Record<string, unknown>>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));\n}\n\n/** A stored lever its AppConfigSchema field refused, with the raw value the doc held. */\nexport interface AppConfigLeverRepair {\n readonly lever: AppConfigLeverKey;\n readonly storedValue: unknown;\n}\n\n/** The doc a writer writes, and every stored lever the merge had to repair to write it. */\nexport interface AppConfigMerge {\n readonly next: AppConfig;\n readonly repairs: readonly AppConfigLeverRepair[];\n}\n\n/**\n * The complete `_appConfig/app` doc to write after applying `update` to `existing` (the doc as\n * read in the writer's transaction; `undefined` or `null` when it does not exist). The update's\n * fields win; `existing` supplies the rest; any field still missing comes from\n * DEFAULT_APP_CONFIG. An `undefined` update field changes nothing. Pure.\n *\n * A stored lever its schema refuses never blocks a save — a console typo must not disable the\n * maintenance kill switch: it is dropped, so the update's value replaces it when the update sends\n * that lever and its DEFAULT_APP_CONFIG value (what every reader already falls back to) otherwise,\n * and it is reported in `repairs`. The release-owned version block is not a lever and has no\n * default, so a refused stored block is still refused.\n *\n * Throws when the result would not parse under AppConfigSchema — an invalid update, or a refused\n * version block — so nothing invalid is ever written.\n */\nexport function mergeAppConfigUpdate(\n existing: Readonly<Record<string, unknown>> | null | undefined,\n update: Readonly<Partial<AppConfig>>,\n): AppConfigMerge {\n const stored = definedFields(existing ?? {});\n const repairs: AppConfigLeverRepair[] = [];\n for (const lever of Object.keys(DEFAULT_APP_CONFIG) as AppConfigLeverKey[]) {\n if (!(lever in stored) || AppConfigSchema.shape[lever].safeParse(stored[lever]).success) continue;\n repairs.push({ lever, storedValue: stored[lever] });\n delete stored[lever];\n }\n const parsed = AppConfigSchema.safeParse({\n ...DEFAULT_APP_CONFIG,\n ...stored,\n ...definedFields(update),\n });\n if (!parsed.success) {\n const issue = parsed.error.issues[0];\n const field = issue?.path.join('.') || 'document';\n throw new Error(\n `_appConfig/app would not match AppConfigSchema after this update (${field}: ${issue?.message ?? 'invalid'}). Nothing should be written.`,\n );\n }\n return { next: parsed.data, repairs };\n}\n\n/**\n * One operator lever off an unvalidated `_appConfig/app` snapshot (the raw stored doc; `null` or\n * `undefined` when it is missing or not yet read): the stored value when its AppConfigSchema\n * field accepts it, otherwise the lever's DEFAULT_APP_CONFIG value — for an absent field and for\n * a value the schema refuses (a wrong type, an over-cap message, an out-of-range multiplier)\n * alike. Pure; never throws.\n */\nexport function readAppConfigLever<L extends AppConfigLeverKey>(\n snapshot: Readonly<Record<string, unknown>> | null | undefined,\n lever: L,\n): AppConfigLevers[L] {\n const parsed = AppConfigSchema.shape[lever].safeParse(snapshot?.[lever]);\n return parsed.success && parsed.data !== undefined\n ? (parsed.data as AppConfigLevers[L])\n : DEFAULT_APP_CONFIG[lever];\n}\n\n/**\n * Every operator lever off an unvalidated `_appConfig/app` snapshot, each read through\n * `readAppConfigLever` — so a field the schema refuses falls back to its default while every\n * valid field survives, and a missing doc reads as DEFAULT_APP_CONFIG: the open posture. Pure;\n * never throws.\n */\nexport function readAppConfigLevers(\n snapshot: Readonly<Record<string, unknown>> | null | undefined,\n): AppConfigLevers {\n const levers: Partial<Record<AppConfigLeverKey, unknown>> = {};\n for (const lever of Object.keys(DEFAULT_APP_CONFIG) as AppConfigLeverKey[]) {\n levers[lever] = readAppConfigLever(snapshot, lever);\n }\n return levers as AppConfigLevers;\n}\n\n/**\n * What an unvalidated `_appConfig/app` snapshot holds in its release-owned version block:\n * - `absent` — no block, so nothing has been released (every document at version 0, nothing\n * required); `block` is `undefined`, which every public-document rule reads that way;\n * - `valid` — the block, parsed;\n * - `invalid` — a block is stored but its AppConfigSchema field refuses it (`null` included);\n * `issue` names the first refused field. It carries no block, so no rule can be computed\n * from it by accident.\n */\nexport type PublicDocumentVersionBlockReading =\n | { readonly status: 'absent'; readonly block: undefined }\n | { readonly status: 'valid'; readonly block: PublicDocumentVersionBlock }\n | { readonly status: 'invalid'; readonly issue: string };\n\nconst ABSENT_PUBLIC_DOCUMENT_VERSION_BLOCK: PublicDocumentVersionBlockReading = Object.freeze({\n status: 'absent',\n block: undefined,\n});\n\n/**\n * The version block off an unvalidated `_appConfig/app` snapshot (the raw stored doc; `null` or\n * `undefined` when it is missing or not yet read), judged by its AppConfigSchema field. Unlike a\n * lever there is no default to fall back to, so an invalid block is reported, never read as\n * absent: what to do about it is the caller's policy. Pure; never throws.\n */\nexport function readPublicDocumentVersionBlock(\n snapshot: Readonly<Record<string, unknown>> | null | undefined,\n): PublicDocumentVersionBlockReading {\n const stored = snapshot?.publicDocumentVersions;\n if (stored === undefined) return ABSENT_PUBLIC_DOCUMENT_VERSION_BLOCK;\n const parsed = AppConfigSchema.shape.publicDocumentVersions.safeParse(stored);\n if (parsed.success && parsed.data !== undefined) return { status: 'valid', block: parsed.data };\n const issue = parsed.success ? undefined : parsed.error.issues[0];\n const field = ['publicDocumentVersions', ...(issue?.path ?? []).map(String)].join('.');\n return { status: 'invalid', issue: `${field}: ${issue?.message ?? 'invalid'}` };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"app-config.js","sourceRoot":"","sources":["../../src/utils/app-config.ts"],"names":[],"mappings":"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","sourcesContent":["// The `_appConfig/app` singleton at rest, and the ONE rule each for writing to it and reading it.\n//\n// WRITE. Several backend writers touch this doc (the admin App configuration card through\n// updateAppConfig, the public-document release publish, a seed), and each sends only the fields\n// it owns. Written as a bare merge, the first of them to run after the doc is gone (a fresh or\n// wiped environment) creates a doc WITHOUT the fields AppConfigSchema requires. The write rule\n// is `mergeAppConfigUpdate(existing, update)`: the writer's update on top of the doc it read,\n// with any field that doc lacks — and any lever it holds at a value its schema refuses — taken\n// from DEFAULT_APP_CONFIG, validated as a whole. A stored version block that does not parse is\n// never written over and never dropped: the writer leaves it as it is (see AppConfigMerge).\n//\n// READ. The stored doc is untrusted — an operator can console-edit any field into any shape —\n// and a config read must never take the product down by itself. The read rule is\n// `readAppConfigLever(snapshot, lever)`: the lever's stored value when its AppConfigSchema field\n// accepts it, otherwise its DEFAULT_APP_CONFIG value, one lever at a time so a bad field never\n// costs the good ones. `readAppConfigLevers` is every lever through it. The backend's cached\n// snapshot reader and the client shell both read through these. The release-owned version block\n// has no default to fall back to, so its reader, `readPublicDocumentVersionBlock`, reports what it\n// found instead — absent, valid, or invalid — and each consumer applies its own policy.\n\nimport { AppConfigSchema, type AppConfig } from '../doc-schemas/system.js';\nimport type { PublicDocumentVersionBlock } from '../doc-schemas/public-documents.js';\n\n/**\n * Every operator lever of `_appConfig/app`, each at a value its AppConfigSchema field accepts.\n * The release-owned `publicDocumentVersions` block is not a lever.\n */\nexport type AppConfigLevers = Readonly<Required<Omit<AppConfig, 'publicDocumentVersions'>>>;\n\n/** The name of one operator lever — a key of DEFAULT_APP_CONFIG. */\nexport type AppConfigLeverKey = keyof AppConfigLevers;\n\n/**\n * Every operator lever of `_appConfig/app` at rest — the doc a fresh environment behaves as if\n * it held, and the value each reader falls back to for a missing or unreadable field. The OPEN\n * posture: maintenance off, registration open, no banner, no throttle.\n *\n * `appVersion` is `''`: no version has been published. VersionGate skips a falsy version, so it\n * stays dormant — no reload for any browser, whatever version it stored before — until an\n * operator sets a real one through updateAppConfig (whose input refuses `''`). From then on the\n * gate compares as usual.\n *\n * The release-owned `publicDocumentVersions` block is not a lever and has no default: it is\n * absent until the first release.\n */\nexport const DEFAULT_APP_CONFIG: AppConfigLevers = Object.freeze({\n appVersion: '',\n maintenanceMode: false,\n maintenanceMessage: '',\n registrationEnabled: true,\n announcementMessage: '',\n rateLimitMultiplier: 1,\n});\n\n/**\n * The line shown while maintenance is on and the operator left `maintenanceMessage` blank — on\n * the maintenance takeover screen and in the callable refusal alike.\n */\nexport const DEFAULT_MAINTENANCE_MESSAGE =\n 'TTT Productions is down for maintenance right now. We will be back shortly — thank you for your patience.';\n\n/**\n * The code the callable maintenance refusal is thrown with. `unavailable` is also what a real\n * outage produces, so the code alone never identifies the refusal — its details do.\n */\nexport const MAINTENANCE_REFUSAL_CODE = 'unavailable' as const;\n\n/** The `details.reason` the callable maintenance refusal carries. */\nexport const MAINTENANCE_REFUSAL_REASON = 'maintenance' as const;\n\n/** Structured details carried by the callable maintenance refusal. */\nexport interface MaintenanceRefusalDetails {\n readonly reason: typeof MAINTENANCE_REFUSAL_REASON;\n}\n\n// The refusal code as the server (`unavailable`) and the Firebase client SDK\n// (`functions/unavailable`) spell it.\nconst MAINTENANCE_REFUSAL_CODES: ReadonlySet<string> = new Set([\n MAINTENANCE_REFUSAL_CODE,\n `functions/${MAINTENANCE_REFUSAL_CODE}`,\n]);\n\n/** True when `details` is the maintenance refusal's structured details. */\nexport function isMaintenanceRefusalDetails(details: unknown): details is MaintenanceRefusalDetails {\n if (!details || typeof details !== 'object') return false;\n return (details as Record<string, unknown>).reason === MAINTENANCE_REFUSAL_REASON;\n}\n\n/**\n * True when `err` is the callable maintenance refusal — an `unavailable` error in either\n * spelling whose details carry `reason: 'maintenance'`. Works on the server-side error and on\n * the client-side callable error alike. Decided by the code and the details, never the message\n * text, so a genuine outage is never mistaken for a designed refusal.\n */\nexport function isMaintenanceRefusalError(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n const e = err as { code?: unknown; details?: unknown };\n return (\n typeof e.code === 'string' && MAINTENANCE_REFUSAL_CODES.has(e.code) && isMaintenanceRefusalDetails(e.details)\n );\n}\n\nfunction definedFields(fields: Readonly<Record<string, unknown>>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));\n}\n\n/** A stored lever its AppConfigSchema field refused, with the raw value the doc held. */\nexport interface AppConfigLeverRepair {\n readonly lever: AppConfigLeverKey;\n readonly storedValue: unknown;\n}\n\n/**\n * The doc a writer writes, every stored lever the merge had to repair to write it, and whether the\n * stored version block must be left as it is.\n */\nexport interface AppConfigMerge {\n readonly next: AppConfig;\n readonly repairs: readonly AppConfigLeverRepair[];\n /**\n * True when the stored version block does not parse and the update does not replace it. `next`\n * then holds no block, and the writer must write `next` as a MERGE (never a full overwrite), so\n * the stored block is kept as it is — the read side keeps reporting it `invalid` (failing closed\n * and alerting) instead of it disappearing and reading as \"nothing released\".\n */\n readonly keepsStoredVersionBlock: boolean;\n}\n\n/**\n * The `_appConfig/app` doc to write after applying `update` to `existing` (the doc as read in the\n * writer's transaction; `undefined` or `null` when it does not exist). The update's fields win;\n * `existing` supplies the rest; any field still missing comes from DEFAULT_APP_CONFIG. An\n * `undefined` update field changes nothing. Pure.\n *\n * A stored value its schema refuses never blocks a save — a console typo must not disable the\n * maintenance kill switch:\n * - a refused LEVER is dropped, so the update's value replaces it when the update sends that lever\n * and its DEFAULT_APP_CONFIG value (what every reader already falls back to) otherwise; it is\n * reported in `repairs`;\n * - a refused version block has no default and must never read as \"nothing released\", so when the\n * update does not send a block it is left untouched (`keepsStoredVersionBlock`); an update that\n * sends a block replaces it.\n *\n * Throws when the update itself would not parse under AppConfigSchema, so nothing invalid is ever\n * written.\n */\nexport function mergeAppConfigUpdate(\n existing: Readonly<Record<string, unknown>> | null | undefined,\n update: Readonly<Partial<AppConfig>>,\n): AppConfigMerge {\n const stored = definedFields(existing ?? {});\n const repairs: AppConfigLeverRepair[] = [];\n for (const lever of Object.keys(DEFAULT_APP_CONFIG) as AppConfigLeverKey[]) {\n if (!(lever in stored) || AppConfigSchema.shape[lever].safeParse(stored[lever]).success) continue;\n repairs.push({ lever, storedValue: stored[lever] });\n delete stored[lever];\n }\n const sent = definedFields(update);\n const keepsStoredVersionBlock =\n 'publicDocumentVersions' in stored &&\n !('publicDocumentVersions' in sent) &&\n !AppConfigSchema.shape.publicDocumentVersions.safeParse(stored.publicDocumentVersions).success;\n if (keepsStoredVersionBlock) delete stored.publicDocumentVersions;\n const parsed = AppConfigSchema.safeParse({ ...DEFAULT_APP_CONFIG, ...stored, ...sent });\n if (!parsed.success) {\n const issue = parsed.error.issues[0];\n const field = issue?.path.join('.') || 'document';\n throw new Error(\n `_appConfig/app would not match AppConfigSchema after this update (${field}: ${issue?.message ?? 'invalid'}). Nothing should be written.`,\n );\n }\n return { next: parsed.data, repairs, keepsStoredVersionBlock };\n}\n\n/**\n * One operator lever off an unvalidated `_appConfig/app` snapshot (the raw stored doc; `null` or\n * `undefined` when it is missing or not yet read): the stored value when its AppConfigSchema\n * field accepts it, otherwise the lever's DEFAULT_APP_CONFIG value — for an absent field and for\n * a value the schema refuses (a wrong type, an over-cap message, an out-of-range multiplier)\n * alike. Pure; never throws.\n */\nexport function readAppConfigLever<L extends AppConfigLeverKey>(\n snapshot: Readonly<Record<string, unknown>> | null | undefined,\n lever: L,\n): AppConfigLevers[L] {\n const parsed = AppConfigSchema.shape[lever].safeParse(snapshot?.[lever]);\n return parsed.success && parsed.data !== undefined\n ? (parsed.data as AppConfigLevers[L])\n : DEFAULT_APP_CONFIG[lever];\n}\n\n/**\n * Every operator lever off an unvalidated `_appConfig/app` snapshot, each read through\n * `readAppConfigLever` — so a field the schema refuses falls back to its default while every\n * valid field survives, and a missing doc reads as DEFAULT_APP_CONFIG: the open posture. Pure;\n * never throws.\n */\nexport function readAppConfigLevers(\n snapshot: Readonly<Record<string, unknown>> | null | undefined,\n): AppConfigLevers {\n const levers: Partial<Record<AppConfigLeverKey, unknown>> = {};\n for (const lever of Object.keys(DEFAULT_APP_CONFIG) as AppConfigLeverKey[]) {\n levers[lever] = readAppConfigLever(snapshot, lever);\n }\n return levers as AppConfigLevers;\n}\n\n/**\n * What an unvalidated `_appConfig/app` snapshot holds in its release-owned version block:\n * - `absent` — no block, so nothing has been released (every document at version 0, nothing\n * required); `block` is `undefined`, which every public-document rule reads that way;\n * - `valid` — the block, parsed;\n * - `invalid` — a block is stored but its AppConfigSchema field refuses it (`null` included);\n * `issue` names the first refused field. It carries no block, so no rule can be computed\n * from it by accident.\n */\nexport type PublicDocumentVersionBlockReading =\n | { readonly status: 'absent'; readonly block: undefined }\n | { readonly status: 'valid'; readonly block: PublicDocumentVersionBlock }\n | { readonly status: 'invalid'; readonly issue: string };\n\nconst ABSENT_PUBLIC_DOCUMENT_VERSION_BLOCK: PublicDocumentVersionBlockReading = Object.freeze({\n status: 'absent',\n block: undefined,\n});\n\n/**\n * The version block off an unvalidated `_appConfig/app` snapshot (the raw stored doc; `null` or\n * `undefined` when it is missing or not yet read), judged by its AppConfigSchema field. Unlike a\n * lever there is no default to fall back to, so an invalid block is reported, never read as\n * absent: what to do about it is the caller's policy. Pure; never throws.\n */\nexport function readPublicDocumentVersionBlock(\n snapshot: Readonly<Record<string, unknown>> | null | undefined,\n): PublicDocumentVersionBlockReading {\n const stored = snapshot?.publicDocumentVersions;\n if (stored === undefined) return ABSENT_PUBLIC_DOCUMENT_VERSION_BLOCK;\n const parsed = AppConfigSchema.shape.publicDocumentVersions.safeParse(stored);\n if (parsed.success && parsed.data !== undefined) return { status: 'valid', block: parsed.data };\n const issue = parsed.success ? undefined : parsed.error.issues[0];\n const field = ['publicDocumentVersions', ...(issue?.path ?? []).map(String)].join('.');\n return { status: 'invalid', issue: `${field}: ${issue?.message ?? 'invalid'}` };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/ttt-core",
|
|
3
|
-
"version": "0.79.
|
|
3
|
+
"version": "0.79.81",
|
|
4
4
|
"description": "Core types, Firestore path constants, and shared constants for TTT Productions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -118,8 +118,8 @@
|
|
|
118
118
|
"@ttt-productions/edge-protocol-core": "^0.1.12",
|
|
119
119
|
"@ttt-productions/input-format-core": "^0.1.1",
|
|
120
120
|
"@ttt-productions/media-schemas": "^0.14.9",
|
|
121
|
-
"@ttt-productions/notification-core": "^0.15.
|
|
122
|
-
"@ttt-productions/report-core": "^0.16.
|
|
121
|
+
"@ttt-productions/notification-core": "^0.15.14",
|
|
122
|
+
"@ttt-productions/report-core": "^0.16.16",
|
|
123
123
|
"zod": "^4.4.3"
|
|
124
124
|
},
|
|
125
125
|
"author": "DJ (TTT Productions)",
|