founder-schema 0.3.0 → 0.4.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/README.md +18 -15
- package/dist/access/access.d.ts +76 -0
- package/dist/access/access.d.ts.map +1 -0
- package/dist/access/access.js +66 -0
- package/dist/access/access.js.map +1 -0
- package/dist/access/permissions.d.ts +153 -0
- package/dist/access/permissions.d.ts.map +1 -0
- package/dist/access/permissions.js +80 -0
- package/dist/access/permissions.js.map +1 -0
- package/dist/access/roles.d.ts +71 -0
- package/dist/access/roles.d.ts.map +1 -0
- package/dist/access/roles.js +72 -0
- package/dist/access/roles.js.map +1 -0
- package/dist/chat.d.ts +4 -4
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +7 -2
- package/dist/chat.js.map +1 -1
- package/dist/core/common.d.ts +304 -0
- package/dist/core/common.d.ts.map +1 -0
- package/dist/core/common.js +340 -0
- package/dist/core/common.js.map +1 -0
- package/dist/core/errors.d.ts +105 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +115 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/limits.d.ts +99 -0
- package/dist/core/limits.d.ts.map +1 -0
- package/dist/core/limits.js +109 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/responses.d.ts +195 -0
- package/dist/core/responses.d.ts.map +1 -0
- package/dist/core/responses.js +73 -0
- package/dist/core/responses.js.map +1 -0
- package/dist/core/validation.d.ts +144 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +150 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/domains/account/account.d.ts +40 -0
- package/dist/domains/account/account.d.ts.map +1 -0
- package/dist/domains/account/account.js +39 -0
- package/dist/domains/account/account.js.map +1 -0
- package/dist/domains/audit/audit.d.ts +169 -0
- package/dist/domains/audit/audit.d.ts.map +1 -0
- package/dist/domains/audit/audit.js +84 -0
- package/dist/domains/audit/audit.js.map +1 -0
- package/dist/domains/chat/chat.d.ts +187 -0
- package/dist/domains/chat/chat.d.ts.map +1 -0
- package/dist/domains/chat/chat.js +189 -0
- package/dist/domains/chat/chat.js.map +1 -0
- package/dist/domains/dashboard/dashboard.d.ts +48 -0
- package/dist/domains/dashboard/dashboard.d.ts.map +1 -0
- package/dist/domains/dashboard/dashboard.js +58 -0
- package/dist/domains/dashboard/dashboard.js.map +1 -0
- package/dist/domains/discover/discover.d.ts +245 -0
- package/dist/domains/discover/discover.d.ts.map +1 -0
- package/dist/domains/discover/discover.js +68 -0
- package/dist/domains/discover/discover.js.map +1 -0
- package/dist/domains/feed/feed.d.ts +1658 -0
- package/dist/domains/feed/feed.d.ts.map +1 -0
- package/dist/domains/feed/feed.js +623 -0
- package/dist/domains/feed/feed.js.map +1 -0
- package/dist/domains/members/members.d.ts +175 -0
- package/dist/domains/members/members.d.ts.map +1 -0
- package/dist/domains/members/members.js +106 -0
- package/dist/domains/members/members.js.map +1 -0
- package/dist/domains/moderation/moderation.d.ts +184 -0
- package/dist/domains/moderation/moderation.d.ts.map +1 -0
- package/dist/domains/moderation/moderation.js +90 -0
- package/dist/domains/moderation/moderation.js.map +1 -0
- package/dist/domains/onboarding/onboarding.d.ts +1018 -0
- package/dist/domains/onboarding/onboarding.d.ts.map +1 -0
- package/dist/domains/onboarding/onboarding.js +717 -0
- package/dist/domains/onboarding/onboarding.js.map +1 -0
- package/dist/domains/profile/founder-profile.d.ts +387 -0
- package/dist/domains/profile/founder-profile.d.ts.map +1 -0
- package/dist/domains/profile/founder-profile.js +180 -0
- package/dist/domains/profile/founder-profile.js.map +1 -0
- package/dist/domains/support/support.d.ts +305 -0
- package/dist/domains/support/support.d.ts.map +1 -0
- package/dist/domains/support/support.js +129 -0
- package/dist/domains/support/support.js.map +1 -0
- package/dist/domains/team/team.d.ts +79 -0
- package/dist/domains/team/team.d.ts.map +1 -0
- package/dist/domains/team/team.js +54 -0
- package/dist/domains/team/team.js.map +1 -0
- package/dist/domains/uploads/uploads.d.ts +113 -0
- package/dist/domains/uploads/uploads.d.ts.map +1 -0
- package/dist/domains/uploads/uploads.js +106 -0
- package/dist/domains/uploads/uploads.js.map +1 -0
- package/dist/env.d.ts +85 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +150 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +21 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -18
- package/dist/index.js.map +1 -1
- package/dist/onboarding.d.ts +58 -32
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +155 -32
- package/dist/onboarding.js.map +1 -1
- package/package.json +14 -2
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema: the audit log
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/audit staff actions, newest first
|
|
5
|
+
*
|
|
6
|
+
* Every action a staff member takes on a MEMBER or on what a member wrote
|
|
7
|
+
* (roles, MPIN resets, suspensions, moderation decisions, taking and closing
|
|
8
|
+
* tickets) is written here, at the moment it happens, by the server: who did
|
|
9
|
+
* it, AS WHICH ROLE, what they did, to what, and anything that explains it.
|
|
10
|
+
* The support team's own library (saved replies) is not a member's, so its
|
|
11
|
+
* edits are not entries. Entries are never edited or
|
|
12
|
+
* deleted. It is how a dispute ("who removed my post, and why") gets an
|
|
13
|
+
* answer, and how a misused staff account is noticed.
|
|
14
|
+
*
|
|
15
|
+
* WHO READS WHAT is the reader's permissions, applied to the query: a
|
|
16
|
+
* superadmin reads every entry; any other staff role reads the entries it
|
|
17
|
+
* wrote itself (its History).
|
|
18
|
+
*
|
|
19
|
+
* The action list grows with the staff features; it never shrinks, because an
|
|
20
|
+
* old entry must still parse.
|
|
21
|
+
*
|
|
22
|
+
* @module founder-schema/domains/audit
|
|
23
|
+
*/
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
import { subjectSchema } from '../../access/permissions.js';
|
|
26
|
+
import { roleSchema } from '../../access/roles.js';
|
|
27
|
+
import { objectIdSchema } from '../../core/common.js';
|
|
28
|
+
import { paginatedSchema } from '../../core/responses.js';
|
|
29
|
+
export const AUDIT_ACTION_VALUES = [
|
|
30
|
+
/** A platform role was granted. `detail.role`. */
|
|
31
|
+
'role.granted',
|
|
32
|
+
/** A platform role was taken away. `detail.role`. */
|
|
33
|
+
'role.revoked',
|
|
34
|
+
/** Someone's forgotten MPIN was cleared, so they can set a new one. */
|
|
35
|
+
'mpin.cleared',
|
|
36
|
+
/** Staff removed someone else's post, poll or comment (`target.subject` says which). */
|
|
37
|
+
'content.removed',
|
|
38
|
+
/** Staff hid a post, poll or comment: out of every feed, restorable. `detail.note`. */
|
|
39
|
+
'content.hidden',
|
|
40
|
+
/** Staff put hidden content back. */
|
|
41
|
+
'content.restored',
|
|
42
|
+
/** Staff looked at what was reported and left it up. `detail.reports`: how many closed. */
|
|
43
|
+
'report.dismissed',
|
|
44
|
+
/** A member was suspended from the product. `detail.reason`. */
|
|
45
|
+
'member.suspended',
|
|
46
|
+
/** A suspension was lifted. */
|
|
47
|
+
'member.unsuspended',
|
|
48
|
+
/** A support agent took a ticket. */
|
|
49
|
+
'ticket.claimed',
|
|
50
|
+
/** A support agent closed a ticket as resolved. */
|
|
51
|
+
'ticket.resolved',
|
|
52
|
+
];
|
|
53
|
+
export const auditActionSchema = z.enum(AUDIT_ACTION_VALUES);
|
|
54
|
+
/**
|
|
55
|
+
* Who did it. A person, acting as one of their roles; or a server script, which
|
|
56
|
+
* is the only way a superadmin is ever made.
|
|
57
|
+
*/
|
|
58
|
+
export const auditActorSchema = z.discriminatedUnion('kind', [
|
|
59
|
+
z.object({
|
|
60
|
+
kind: z.literal('staff'),
|
|
61
|
+
id: objectIdSchema,
|
|
62
|
+
/** Their name when they did it. An entry is a record, so it is not renamed later. */
|
|
63
|
+
displayName: z.string(),
|
|
64
|
+
/** The role they were acting as. */
|
|
65
|
+
actingAs: roleSchema,
|
|
66
|
+
}),
|
|
67
|
+
z.object({
|
|
68
|
+
kind: z.literal('script'),
|
|
69
|
+
/** Which script, e.g. `grant-superadmin`. */
|
|
70
|
+
name: z.string(),
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
export const auditEntrySchema = z.object({
|
|
74
|
+
id: objectIdSchema,
|
|
75
|
+
actor: auditActorSchema,
|
|
76
|
+
action: auditActionSchema,
|
|
77
|
+
/** What it was done to. `label` is its name at the time, when it has one. */
|
|
78
|
+
target: z.object({ subject: subjectSchema, id: objectIdSchema, label: z.string().nullable() }),
|
|
79
|
+
/** The specifics, by action (the role granted, a reason given). Never a secret. */
|
|
80
|
+
detail: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])),
|
|
81
|
+
at: z.iso.datetime(),
|
|
82
|
+
});
|
|
83
|
+
export const auditPageSchema = paginatedSchema(auditEntrySchema);
|
|
84
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/domains/audit/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,kDAAkD;IAClD,cAAc;IACd,qDAAqD;IACrD,cAAc;IACd,uEAAuE;IACvE,cAAc;IACd,wFAAwF;IACxF,iBAAiB;IACjB,uFAAuF;IACvF,gBAAgB;IAChB,qCAAqC;IACrC,kBAAkB;IAClB,2FAA2F;IAC3F,kBAAkB;IAClB,gEAAgE;IAChE,kBAAkB;IAClB,+BAA+B;IAC/B,oBAAoB;IACpB,qCAAqC;IACrC,gBAAgB;IAChB,mDAAmD;IACnD,iBAAiB;CACT,CAAC;AACX,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;AAG7D;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;IAC3D,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QACxB,EAAE,EAAE,cAAc;QAClB,qFAAqF;QACrF,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,oCAAoC;QACpC,QAAQ,EAAE,UAAU;KACrB,CAAC;IACF,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;QACzB,6CAA6C;QAC7C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;KACjB,CAAC;CACH,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,cAAc;IAClB,KAAK,EAAE,gBAAgB;IACvB,MAAM,EAAE,iBAAiB;IACzB,6EAA6E;IAC7E,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC;IAC9F,mFAAmF;IACnF,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACtF,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CACrB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAC,gBAAgB,CAAC,CAAC","sourcesContent":["/**\n * founder-schema: the audit log\n *\n * GET /api/v1/audit staff actions, newest first\n *\n * Every action a staff member takes on a MEMBER or on what a member wrote\n * (roles, MPIN resets, suspensions, moderation decisions, taking and closing\n * tickets) is written here, at the moment it happens, by the server: who did\n * it, AS WHICH ROLE, what they did, to what, and anything that explains it.\n * The support team's own library (saved replies) is not a member's, so its\n * edits are not entries. Entries are never edited or\n * deleted. It is how a dispute (\"who removed my post, and why\") gets an\n * answer, and how a misused staff account is noticed.\n *\n * WHO READS WHAT is the reader's permissions, applied to the query: a\n * superadmin reads every entry; any other staff role reads the entries it\n * wrote itself (its History).\n *\n * The action list grows with the staff features; it never shrinks, because an\n * old entry must still parse.\n *\n * @module founder-schema/domains/audit\n */\n\nimport { z } from 'zod';\nimport { subjectSchema } from '../../access/permissions.js';\nimport { roleSchema } from '../../access/roles.js';\nimport { objectIdSchema } from '../../core/common.js';\nimport { paginatedSchema } from '../../core/responses.js';\n\nexport const AUDIT_ACTION_VALUES = [\n /** A platform role was granted. `detail.role`. */\n 'role.granted',\n /** A platform role was taken away. `detail.role`. */\n 'role.revoked',\n /** Someone's forgotten MPIN was cleared, so they can set a new one. */\n 'mpin.cleared',\n /** Staff removed someone else's post, poll or comment (`target.subject` says which). */\n 'content.removed',\n /** Staff hid a post, poll or comment: out of every feed, restorable. `detail.note`. */\n 'content.hidden',\n /** Staff put hidden content back. */\n 'content.restored',\n /** Staff looked at what was reported and left it up. `detail.reports`: how many closed. */\n 'report.dismissed',\n /** A member was suspended from the product. `detail.reason`. */\n 'member.suspended',\n /** A suspension was lifted. */\n 'member.unsuspended',\n /** A support agent took a ticket. */\n 'ticket.claimed',\n /** A support agent closed a ticket as resolved. */\n 'ticket.resolved',\n] as const;\nexport const auditActionSchema = z.enum(AUDIT_ACTION_VALUES);\nexport type AuditAction = z.infer<typeof auditActionSchema>;\n\n/**\n * Who did it. A person, acting as one of their roles; or a server script, which\n * is the only way a superadmin is ever made.\n */\nexport const auditActorSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('staff'),\n id: objectIdSchema,\n /** Their name when they did it. An entry is a record, so it is not renamed later. */\n displayName: z.string(),\n /** The role they were acting as. */\n actingAs: roleSchema,\n }),\n z.object({\n kind: z.literal('script'),\n /** Which script, e.g. `grant-superadmin`. */\n name: z.string(),\n }),\n]);\nexport type AuditActor = z.infer<typeof auditActorSchema>;\n\nexport const auditEntrySchema = z.object({\n id: objectIdSchema,\n actor: auditActorSchema,\n action: auditActionSchema,\n /** What it was done to. `label` is its name at the time, when it has one. */\n target: z.object({ subject: subjectSchema, id: objectIdSchema, label: z.string().nullable() }),\n /** The specifics, by action (the role granted, a reason given). Never a secret. */\n detail: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])),\n at: z.iso.datetime(),\n});\nexport type AuditEntry = z.infer<typeof auditEntrySchema>;\n\nexport const auditPageSchema = paginatedSchema(auditEntrySchema);\nexport type AuditPage = z.infer<typeof auditPageSchema>;\n"]}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Chat: config, auth token, opening a conversation, participants
|
|
3
|
+
*
|
|
4
|
+
* Founder Circle does not run a chat server. It uses the shared one, which
|
|
5
|
+
* serves several products, and that is the whole reason this file is careful:
|
|
6
|
+
* when one server serves many tenants, the failure mode is SILENT. A push once
|
|
7
|
+
* returned success having sent nothing, because the caller held the wrong
|
|
8
|
+
* product's key and nothing in the exchange said which product it was for.
|
|
9
|
+
*
|
|
10
|
+
* So the rule these shapes exist to hold: TENANCY COMES FROM THE CREDENTIAL,
|
|
11
|
+
* never from a request body. The token says which product it is for, the chat
|
|
12
|
+
* server resolves the product from the token and from nothing else, and a
|
|
13
|
+
* Founder Circle token cannot address another product's conversation even when
|
|
14
|
+
* handed a valid-looking id.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ TEXT ONLY. There is deliberately no `callsEnabled` flag here, and its
|
|
17
|
+
* absence is the feature: the user ruled chat is text only, there is no call
|
|
18
|
+
* code to branch on, and the browser is told to deny camera and microphone
|
|
19
|
+
* outright. A boolean that can never be true is an invitation to turn it on.
|
|
20
|
+
*
|
|
21
|
+
* @module founder-schema/domains/chat
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
/**
|
|
25
|
+
* The product this package speaks for.
|
|
26
|
+
*
|
|
27
|
+
* Typed as a LITERAL on purpose. If the shared chat server ever hands back a
|
|
28
|
+
* config or a token minted for a different product, parsing fails loudly here
|
|
29
|
+
* rather than the client quietly talking to the wrong tenant. That is exactly
|
|
30
|
+
* the class of bug this file exists to prevent, so it should be impossible to
|
|
31
|
+
* carry past the first parse.
|
|
32
|
+
*/
|
|
33
|
+
export declare const CHAT_PRODUCT_ID: "founder-circle";
|
|
34
|
+
export declare const chatProductIdSchema: z.ZodLiteral<"founder-circle">;
|
|
35
|
+
export type ChatProductId = z.infer<typeof chatProductIdSchema>;
|
|
36
|
+
/**
|
|
37
|
+
* What the client needs before it can open a socket.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ `enabled` IS MANDATORY AND IS NOT DERIVED FROM `url`. "Messaging is not
|
|
40
|
+
* switched on yet" is a first-class state, not a failure: a 404 is the expected
|
|
41
|
+
* answer today, and the client collapses a disabled flag, a null url, a non-2xx
|
|
42
|
+
* and a thrown fetch to the same quiet "not on yet" rather than an error. If
|
|
43
|
+
* `enabled` were inferred from a missing url, "off" and "broken" would be the
|
|
44
|
+
* same wire shape and the inbox could not tell a member which one it was.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ `userId` is required and is the field nothing works without. The client
|
|
47
|
+
* needs to know WHO THE VIEWER IS to decide which side a bubble sits on, to
|
|
48
|
+
* filter the viewer out of presence and typing, to pick the other participant
|
|
49
|
+
* for a row's name and avatar, and to tell its own optimistic message from one
|
|
50
|
+
* echoed back. Without it every conversation renders as though the viewer were
|
|
51
|
+
* nobody. It was missing from the first sketch of this endpoint.
|
|
52
|
+
*/
|
|
53
|
+
export declare const chatConfigSchema: z.ZodObject<{
|
|
54
|
+
enabled: z.ZodBoolean;
|
|
55
|
+
url: z.ZodNullable<z.ZodURL>;
|
|
56
|
+
userId: z.ZodString;
|
|
57
|
+
productId: z.ZodLiteral<"founder-circle">;
|
|
58
|
+
}, z.core.$strip>;
|
|
59
|
+
export type ChatConfig = z.infer<typeof chatConfigSchema>;
|
|
60
|
+
/**
|
|
61
|
+
* How long before expiry the client re-mints, in seconds.
|
|
62
|
+
*
|
|
63
|
+
* Exported because it is a contract detail and not an implementation one: a
|
|
64
|
+
* token handed over with two seconds left is a socket that authenticates and is
|
|
65
|
+
* closed again immediately, so the minting side needs to know the consuming
|
|
66
|
+
* side will not use the last minute of a token's life.
|
|
67
|
+
*/
|
|
68
|
+
export declare const CHAT_TOKEN_REFRESH_MARGIN_SECONDS = 60;
|
|
69
|
+
/**
|
|
70
|
+
* How often the live socket re-authenticates, in seconds.
|
|
71
|
+
*
|
|
72
|
+
* Auth is an EVENT on the open socket rather than a handshake parameter, so
|
|
73
|
+
* re-authenticating does not drop the connection.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ THIS MUST STAY BELOW THE REAL TOKEN LIFETIME, by more than the refresh
|
|
76
|
+
* margin. It assumes a 15 minute token. A re-auth interval longer than the
|
|
77
|
+
* lifetime is a socket that dies every quarter of an hour with no explanation,
|
|
78
|
+
* and the symptom appears far from the cause. If the issued lifetime changes,
|
|
79
|
+
* this number moves with it or the two silently disagree.
|
|
80
|
+
*/
|
|
81
|
+
export declare const CHAT_SOCKET_REAUTH_SECONDS: number;
|
|
82
|
+
/**
|
|
83
|
+
* A short-lived token for the shared chat server.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ BOTH TIME FIELDS ARE KEPT, and they are not redundant, because two
|
|
86
|
+
* different parties read them with two different clocks:
|
|
87
|
+
*
|
|
88
|
+
* `expiresAt` an absolute instant, for the SERVER. The chat server holds the
|
|
89
|
+
* authoritative clock and must reject an expired token on its
|
|
90
|
+
* own, whatever any client believes.
|
|
91
|
+
* `expiresIn` a duration in seconds, for the CLIENT. Scheduling a refresh
|
|
92
|
+
* from `receivedAt + expiresIn` uses only the client's own
|
|
93
|
+
* elapsed time, so a device whose absolute clock is wrong still
|
|
94
|
+
* refreshes correctly. Scheduling from `expiresAt` instead means
|
|
95
|
+
* a browser an hour fast treats every fresh token as already
|
|
96
|
+
* expired and re-mints forever.
|
|
97
|
+
*
|
|
98
|
+
* Dropping either one breaks the party that reads it. This is the same reason
|
|
99
|
+
* OAuth carries `expires_in` on the wire while the token itself carries `exp`.
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ AND THE CLIENT IS NOT THE ENFORCER. Its margin is an optimisation to avoid
|
|
102
|
+
* a pointless round trip. A wrong clock, a tab asleep for an hour, or a modified
|
|
103
|
+
* client all defeat anything the client does, so the chat server rejects an
|
|
104
|
+
* expired token on its own.
|
|
105
|
+
*/
|
|
106
|
+
export declare const chatAuthTokenSchema: z.ZodObject<{
|
|
107
|
+
token: z.ZodString;
|
|
108
|
+
expiresAt: z.ZodISODateTime;
|
|
109
|
+
expiresIn: z.ZodNumber;
|
|
110
|
+
productId: z.ZodLiteral<"founder-circle">;
|
|
111
|
+
}, z.core.$strip>;
|
|
112
|
+
export type ChatAuthToken = z.infer<typeof chatAuthTokenSchema>;
|
|
113
|
+
/**
|
|
114
|
+
* `POST /api/v1/chat/open`: start (or find) the direct conversation with
|
|
115
|
+
* another member.
|
|
116
|
+
*
|
|
117
|
+
* Our backend decides who may talk to whom, because only it knows this
|
|
118
|
+
* product's rules, and it does the whole exchange with the chat server itself:
|
|
119
|
+
* it mints the viewer's token and an invitation naming both participants, and
|
|
120
|
+
* redeems them server to server. No credential reaches the browser.
|
|
121
|
+
*/
|
|
122
|
+
export declare const openChatBodySchema: z.ZodObject<{
|
|
123
|
+
userId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
124
|
+
}, z.core.$strip>;
|
|
125
|
+
export type OpenChatBody = z.infer<typeof openChatBodySchema>;
|
|
126
|
+
/**
|
|
127
|
+
* The conversation to navigate to. `201` with `wasCreated: true` the first
|
|
128
|
+
* time, `200` and `false` on every later open of the same pair.
|
|
129
|
+
*/
|
|
130
|
+
export declare const openChatResponseSchema: z.ZodObject<{
|
|
131
|
+
conversationId: z.ZodString;
|
|
132
|
+
wasCreated: z.ZodBoolean;
|
|
133
|
+
}, z.core.$strip>;
|
|
134
|
+
export type OpenChatResponse = z.infer<typeof openChatResponseSchema>;
|
|
135
|
+
/**
|
|
136
|
+
* `POST /api/v1/chat/participants`: names and avatars for the people in the
|
|
137
|
+
* viewer's conversations.
|
|
138
|
+
*
|
|
139
|
+
* The chat server knows ids and nothing about who anybody is, because it serves
|
|
140
|
+
* several products and identity belongs to each; so the conversation list asks
|
|
141
|
+
* here, once per screen. Bounded by `CHAT_PARTICIPANTS_MAX`, so it cannot page
|
|
142
|
+
* through the directory.
|
|
143
|
+
*/
|
|
144
|
+
export declare const chatParticipantsBodySchema: z.ZodObject<{
|
|
145
|
+
userIds: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
146
|
+
}, z.core.$strip>;
|
|
147
|
+
export type ChatParticipantsBody = z.infer<typeof chatParticipantsBodySchema>;
|
|
148
|
+
/** The public card fields any member already sees, and nothing more. */
|
|
149
|
+
export declare const chatParticipantSchema: z.ZodObject<{
|
|
150
|
+
displayName: z.ZodString;
|
|
151
|
+
username: z.ZodString;
|
|
152
|
+
avatarUrl: z.ZodNullable<z.ZodURL>;
|
|
153
|
+
}, z.core.$strip>;
|
|
154
|
+
export type ChatParticipant = z.infer<typeof chatParticipantSchema>;
|
|
155
|
+
/**
|
|
156
|
+
* Keyed by user id. ⚠ An id with no active, onboarded profile is ABSENT, not
|
|
157
|
+
* an error: the client renders it as a former member rather than guessing.
|
|
158
|
+
*/
|
|
159
|
+
export declare const chatParticipantsResponseSchema: z.ZodObject<{
|
|
160
|
+
participants: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
161
|
+
displayName: z.ZodString;
|
|
162
|
+
username: z.ZodString;
|
|
163
|
+
avatarUrl: z.ZodNullable<z.ZodURL>;
|
|
164
|
+
}, z.core.$strip>>;
|
|
165
|
+
}, z.core.$strip>;
|
|
166
|
+
export type ChatParticipantsResponse = z.infer<typeof chatParticipantsResponseSchema>;
|
|
167
|
+
/**
|
|
168
|
+
* @deprecated Since 0.2.0: use `openChatBodySchema`, the same shape under the
|
|
169
|
+
* name of the route that takes it.
|
|
170
|
+
*/
|
|
171
|
+
export declare const chatInvitationBodySchema: z.ZodObject<{
|
|
172
|
+
userId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
173
|
+
}, z.core.$strip>;
|
|
174
|
+
/** @deprecated Since 0.2.0: use `OpenChatBody`. */
|
|
175
|
+
export type ChatInvitationBody = OpenChatBody;
|
|
176
|
+
/**
|
|
177
|
+
* @deprecated Since 0.2.0, and it describes nothing that exists. The browser
|
|
178
|
+
* never holds an invitation: `/chat/open` mints and redeems it server to
|
|
179
|
+
* server. The old `/invitation` route that handed one out could never have
|
|
180
|
+
* worked and is gone.
|
|
181
|
+
*/
|
|
182
|
+
export declare const chatInvitationSchema: z.ZodObject<{
|
|
183
|
+
invitationToken: z.ZodString;
|
|
184
|
+
}, z.core.$strip>;
|
|
185
|
+
/** @deprecated Since 0.2.0. */
|
|
186
|
+
export type ChatInvitation = z.infer<typeof chatInvitationSchema>;
|
|
187
|
+
//# sourceMappingURL=chat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/domains/chat/chat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,EAAG,gBAAyB,CAAC;AACzD,eAAO,MAAM,mBAAmB,gCAA6B,CAAC;AAC9D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB;;;;;iBAQ3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,KAAK,CAAC;AAEpD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,QAAU,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB;;;;;iBAQ9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB;;iBAG7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;iBAGjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B;;iBAErC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,wEAAwE;AACxE,eAAO,MAAM,qBAAqB;;;;iBAKhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;GAGG;AACH,eAAO,MAAM,8BAA8B;;;;;;iBAEzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAStF;;;GAGG;AACH,eAAO,MAAM,wBAAwB;;iBAAqB,CAAC;AAC3D,mDAAmD;AACnD,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;iBAE/B,CAAC;AACH,+BAA+B;AAC/B,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Chat: config, auth token, opening a conversation, participants
|
|
3
|
+
*
|
|
4
|
+
* Founder Circle does not run a chat server. It uses the shared one, which
|
|
5
|
+
* serves several products, and that is the whole reason this file is careful:
|
|
6
|
+
* when one server serves many tenants, the failure mode is SILENT. A push once
|
|
7
|
+
* returned success having sent nothing, because the caller held the wrong
|
|
8
|
+
* product's key and nothing in the exchange said which product it was for.
|
|
9
|
+
*
|
|
10
|
+
* So the rule these shapes exist to hold: TENANCY COMES FROM THE CREDENTIAL,
|
|
11
|
+
* never from a request body. The token says which product it is for, the chat
|
|
12
|
+
* server resolves the product from the token and from nothing else, and a
|
|
13
|
+
* Founder Circle token cannot address another product's conversation even when
|
|
14
|
+
* handed a valid-looking id.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ TEXT ONLY. There is deliberately no `callsEnabled` flag here, and its
|
|
17
|
+
* absence is the feature: the user ruled chat is text only, there is no call
|
|
18
|
+
* code to branch on, and the browser is told to deny camera and microphone
|
|
19
|
+
* outright. A boolean that can never be true is an invitation to turn it on.
|
|
20
|
+
*
|
|
21
|
+
* @module founder-schema/domains/chat
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { objectIdSchema, usernameSchema } from '../../core/common.js';
|
|
25
|
+
import { CHAT_PARTICIPANTS_MAX } from '../../core/limits.js';
|
|
26
|
+
/**
|
|
27
|
+
* The product this package speaks for.
|
|
28
|
+
*
|
|
29
|
+
* Typed as a LITERAL on purpose. If the shared chat server ever hands back a
|
|
30
|
+
* config or a token minted for a different product, parsing fails loudly here
|
|
31
|
+
* rather than the client quietly talking to the wrong tenant. That is exactly
|
|
32
|
+
* the class of bug this file exists to prevent, so it should be impossible to
|
|
33
|
+
* carry past the first parse.
|
|
34
|
+
*/
|
|
35
|
+
export const CHAT_PRODUCT_ID = 'founder-circle';
|
|
36
|
+
export const chatProductIdSchema = z.literal(CHAT_PRODUCT_ID);
|
|
37
|
+
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* What the client needs before it can open a socket.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ `enabled` IS MANDATORY AND IS NOT DERIVED FROM `url`. "Messaging is not
|
|
42
|
+
* switched on yet" is a first-class state, not a failure: a 404 is the expected
|
|
43
|
+
* answer today, and the client collapses a disabled flag, a null url, a non-2xx
|
|
44
|
+
* and a thrown fetch to the same quiet "not on yet" rather than an error. If
|
|
45
|
+
* `enabled` were inferred from a missing url, "off" and "broken" would be the
|
|
46
|
+
* same wire shape and the inbox could not tell a member which one it was.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ `userId` is required and is the field nothing works without. The client
|
|
49
|
+
* needs to know WHO THE VIEWER IS to decide which side a bubble sits on, to
|
|
50
|
+
* filter the viewer out of presence and typing, to pick the other participant
|
|
51
|
+
* for a row's name and avatar, and to tell its own optimistic message from one
|
|
52
|
+
* echoed back. Without it every conversation renders as though the viewer were
|
|
53
|
+
* nobody. It was missing from the first sketch of this endpoint.
|
|
54
|
+
*/
|
|
55
|
+
export const chatConfigSchema = z.object({
|
|
56
|
+
enabled: z.boolean(),
|
|
57
|
+
/** Null whenever chat is not switched on. Never a placeholder string. */
|
|
58
|
+
url: z.url().nullable(),
|
|
59
|
+
/** The VIEWER's id, as the chat server knows them. */
|
|
60
|
+
userId: z.string(),
|
|
61
|
+
/** Which product this config is for. See `chatProductIdSchema`. */
|
|
62
|
+
productId: chatProductIdSchema,
|
|
63
|
+
});
|
|
64
|
+
// ─── Auth token ──────────────────────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* How long before expiry the client re-mints, in seconds.
|
|
67
|
+
*
|
|
68
|
+
* Exported because it is a contract detail and not an implementation one: a
|
|
69
|
+
* token handed over with two seconds left is a socket that authenticates and is
|
|
70
|
+
* closed again immediately, so the minting side needs to know the consuming
|
|
71
|
+
* side will not use the last minute of a token's life.
|
|
72
|
+
*/
|
|
73
|
+
export const CHAT_TOKEN_REFRESH_MARGIN_SECONDS = 60;
|
|
74
|
+
/**
|
|
75
|
+
* How often the live socket re-authenticates, in seconds.
|
|
76
|
+
*
|
|
77
|
+
* Auth is an EVENT on the open socket rather than a handshake parameter, so
|
|
78
|
+
* re-authenticating does not drop the connection.
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ THIS MUST STAY BELOW THE REAL TOKEN LIFETIME, by more than the refresh
|
|
81
|
+
* margin. It assumes a 15 minute token. A re-auth interval longer than the
|
|
82
|
+
* lifetime is a socket that dies every quarter of an hour with no explanation,
|
|
83
|
+
* and the symptom appears far from the cause. If the issued lifetime changes,
|
|
84
|
+
* this number moves with it or the two silently disagree.
|
|
85
|
+
*/
|
|
86
|
+
export const CHAT_SOCKET_REAUTH_SECONDS = 10 * 60;
|
|
87
|
+
/**
|
|
88
|
+
* A short-lived token for the shared chat server.
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ BOTH TIME FIELDS ARE KEPT, and they are not redundant, because two
|
|
91
|
+
* different parties read them with two different clocks:
|
|
92
|
+
*
|
|
93
|
+
* `expiresAt` an absolute instant, for the SERVER. The chat server holds the
|
|
94
|
+
* authoritative clock and must reject an expired token on its
|
|
95
|
+
* own, whatever any client believes.
|
|
96
|
+
* `expiresIn` a duration in seconds, for the CLIENT. Scheduling a refresh
|
|
97
|
+
* from `receivedAt + expiresIn` uses only the client's own
|
|
98
|
+
* elapsed time, so a device whose absolute clock is wrong still
|
|
99
|
+
* refreshes correctly. Scheduling from `expiresAt` instead means
|
|
100
|
+
* a browser an hour fast treats every fresh token as already
|
|
101
|
+
* expired and re-mints forever.
|
|
102
|
+
*
|
|
103
|
+
* Dropping either one breaks the party that reads it. This is the same reason
|
|
104
|
+
* OAuth carries `expires_in` on the wire while the token itself carries `exp`.
|
|
105
|
+
*
|
|
106
|
+
* ⚠️ AND THE CLIENT IS NOT THE ENFORCER. Its margin is an optimisation to avoid
|
|
107
|
+
* a pointless round trip. A wrong clock, a tab asleep for an hour, or a modified
|
|
108
|
+
* client all defeat anything the client does, so the chat server rejects an
|
|
109
|
+
* expired token on its own.
|
|
110
|
+
*/
|
|
111
|
+
export const chatAuthTokenSchema = z.object({
|
|
112
|
+
token: z.string().min(1),
|
|
113
|
+
/** ISO 8601. Parsed with `Date.parse`, never an epoch number. */
|
|
114
|
+
expiresAt: z.iso.datetime(),
|
|
115
|
+
/** Seconds from issue. See the note above on why both exist. */
|
|
116
|
+
expiresIn: z.number().int().positive(),
|
|
117
|
+
/** The product this token is scoped to. A mismatch must fail loudly. */
|
|
118
|
+
productId: chatProductIdSchema,
|
|
119
|
+
});
|
|
120
|
+
// ─── Opening a conversation ──────────────────────────────────────────────────
|
|
121
|
+
/**
|
|
122
|
+
* `POST /api/v1/chat/open`: start (or find) the direct conversation with
|
|
123
|
+
* another member.
|
|
124
|
+
*
|
|
125
|
+
* Our backend decides who may talk to whom, because only it knows this
|
|
126
|
+
* product's rules, and it does the whole exchange with the chat server itself:
|
|
127
|
+
* it mints the viewer's token and an invitation naming both participants, and
|
|
128
|
+
* redeems them server to server. No credential reaches the browser.
|
|
129
|
+
*/
|
|
130
|
+
export const openChatBodySchema = z.object({
|
|
131
|
+
/** The OTHER participant. The viewer is taken from the credential, never the body. */
|
|
132
|
+
userId: objectIdSchema,
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* The conversation to navigate to. `201` with `wasCreated: true` the first
|
|
136
|
+
* time, `200` and `false` on every later open of the same pair.
|
|
137
|
+
*/
|
|
138
|
+
export const openChatResponseSchema = z.object({
|
|
139
|
+
conversationId: z.string().min(1),
|
|
140
|
+
wasCreated: z.boolean(),
|
|
141
|
+
});
|
|
142
|
+
// ─── Participants ────────────────────────────────────────────────────────────
|
|
143
|
+
/**
|
|
144
|
+
* `POST /api/v1/chat/participants`: names and avatars for the people in the
|
|
145
|
+
* viewer's conversations.
|
|
146
|
+
*
|
|
147
|
+
* The chat server knows ids and nothing about who anybody is, because it serves
|
|
148
|
+
* several products and identity belongs to each; so the conversation list asks
|
|
149
|
+
* here, once per screen. Bounded by `CHAT_PARTICIPANTS_MAX`, so it cannot page
|
|
150
|
+
* through the directory.
|
|
151
|
+
*/
|
|
152
|
+
export const chatParticipantsBodySchema = z.object({
|
|
153
|
+
userIds: z.array(objectIdSchema).min(1).max(CHAT_PARTICIPANTS_MAX),
|
|
154
|
+
});
|
|
155
|
+
/** The public card fields any member already sees, and nothing more. */
|
|
156
|
+
export const chatParticipantSchema = z.object({
|
|
157
|
+
displayName: z.string(),
|
|
158
|
+
username: usernameSchema,
|
|
159
|
+
/** Null when the member has no photo. Ours, or from the login provider. */
|
|
160
|
+
avatarUrl: z.url().nullable(),
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* Keyed by user id. ⚠ An id with no active, onboarded profile is ABSENT, not
|
|
164
|
+
* an error: the client renders it as a former member rather than guessing.
|
|
165
|
+
*/
|
|
166
|
+
export const chatParticipantsResponseSchema = z.object({
|
|
167
|
+
participants: z.record(z.string(), chatParticipantSchema),
|
|
168
|
+
});
|
|
169
|
+
// ─── Deprecated ──────────────────────────────────────────────────────────────
|
|
170
|
+
//
|
|
171
|
+
// Unused by every consumer, and still exported: removing an export trips the
|
|
172
|
+
// publish rule that refuses a release missing anything the live one has, so it
|
|
173
|
+
// is the user's decision, not a side effect. They were meant to go in 0.3.0;
|
|
174
|
+
// that release kept them.
|
|
175
|
+
/**
|
|
176
|
+
* @deprecated Since 0.2.0: use `openChatBodySchema`, the same shape under the
|
|
177
|
+
* name of the route that takes it.
|
|
178
|
+
*/
|
|
179
|
+
export const chatInvitationBodySchema = openChatBodySchema;
|
|
180
|
+
/**
|
|
181
|
+
* @deprecated Since 0.2.0, and it describes nothing that exists. The browser
|
|
182
|
+
* never holds an invitation: `/chat/open` mints and redeems it server to
|
|
183
|
+
* server. The old `/invitation` route that handed one out could never have
|
|
184
|
+
* worked and is gone.
|
|
185
|
+
*/
|
|
186
|
+
export const chatInvitationSchema = z.object({
|
|
187
|
+
invitationToken: z.string().min(1),
|
|
188
|
+
});
|
|
189
|
+
//# sourceMappingURL=chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../../../src/domains/chat/chat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7D;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,gBAAyB,CAAC;AACzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;AAG9D,gFAAgF;AAEhF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB,yEAAyE;IACzE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACvB,sDAAsD;IACtD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,mEAAmE;IACnE,SAAS,EAAE,mBAAmB;CAC/B,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,CAAC;AAEpD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,GAAG,EAAE,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,iEAAiE;IACjE,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IAC3B,gEAAgE;IAChE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACtC,wEAAwE;IACxE,SAAS,EAAE,mBAAmB;CAC/B,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,sFAAsF;IACtF,MAAM,EAAE,cAAc;CACvB,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE;CACxB,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,qBAAqB,CAAC;CACnE,CAAC,CAAC;AAGH,wEAAwE;AACxE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;IACvB,QAAQ,EAAE,cAAc;IACxB,2EAA2E;IAC3E,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC;IACrD,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,qBAAqB,CAAC;CAC1D,CAAC,CAAC;AAGH,gFAAgF;AAChF,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,6EAA6E;AAC7E,0BAA0B;AAE1B;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,kBAAkB,CAAC;AAI3D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACnC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema — Chat: config, auth token, opening a conversation, participants\n *\n * Founder Circle does not run a chat server. It uses the shared one, which\n * serves several products, and that is the whole reason this file is careful:\n * when one server serves many tenants, the failure mode is SILENT. A push once\n * returned success having sent nothing, because the caller held the wrong\n * product's key and nothing in the exchange said which product it was for.\n *\n * So the rule these shapes exist to hold: TENANCY COMES FROM THE CREDENTIAL,\n * never from a request body. The token says which product it is for, the chat\n * server resolves the product from the token and from nothing else, and a\n * Founder Circle token cannot address another product's conversation even when\n * handed a valid-looking id.\n *\n * ⚠️ TEXT ONLY. There is deliberately no `callsEnabled` flag here, and its\n * absence is the feature: the user ruled chat is text only, there is no call\n * code to branch on, and the browser is told to deny camera and microphone\n * outright. A boolean that can never be true is an invitation to turn it on.\n *\n * @module founder-schema/domains/chat\n */\n\nimport { z } from 'zod';\nimport { objectIdSchema, usernameSchema } from '../../core/common.js';\nimport { CHAT_PARTICIPANTS_MAX } from '../../core/limits.js';\n\n/**\n * The product this package speaks for.\n *\n * Typed as a LITERAL on purpose. If the shared chat server ever hands back a\n * config or a token minted for a different product, parsing fails loudly here\n * rather than the client quietly talking to the wrong tenant. That is exactly\n * the class of bug this file exists to prevent, so it should be impossible to\n * carry past the first parse.\n */\nexport const CHAT_PRODUCT_ID = 'founder-circle' as const;\nexport const chatProductIdSchema = z.literal(CHAT_PRODUCT_ID);\nexport type ChatProductId = z.infer<typeof chatProductIdSchema>;\n\n// ─── Config ──────────────────────────────────────────────────────────────────\n\n/**\n * What the client needs before it can open a socket.\n *\n * ⚠️ `enabled` IS MANDATORY AND IS NOT DERIVED FROM `url`. \"Messaging is not\n * switched on yet\" is a first-class state, not a failure: a 404 is the expected\n * answer today, and the client collapses a disabled flag, a null url, a non-2xx\n * and a thrown fetch to the same quiet \"not on yet\" rather than an error. If\n * `enabled` were inferred from a missing url, \"off\" and \"broken\" would be the\n * same wire shape and the inbox could not tell a member which one it was.\n *\n * ⚠️ `userId` is required and is the field nothing works without. The client\n * needs to know WHO THE VIEWER IS to decide which side a bubble sits on, to\n * filter the viewer out of presence and typing, to pick the other participant\n * for a row's name and avatar, and to tell its own optimistic message from one\n * echoed back. Without it every conversation renders as though the viewer were\n * nobody. It was missing from the first sketch of this endpoint.\n */\nexport const chatConfigSchema = z.object({\n enabled: z.boolean(),\n /** Null whenever chat is not switched on. Never a placeholder string. */\n url: z.url().nullable(),\n /** The VIEWER's id, as the chat server knows them. */\n userId: z.string(),\n /** Which product this config is for. See `chatProductIdSchema`. */\n productId: chatProductIdSchema,\n});\nexport type ChatConfig = z.infer<typeof chatConfigSchema>;\n\n// ─── Auth token ──────────────────────────────────────────────────────────────\n\n/**\n * How long before expiry the client re-mints, in seconds.\n *\n * Exported because it is a contract detail and not an implementation one: a\n * token handed over with two seconds left is a socket that authenticates and is\n * closed again immediately, so the minting side needs to know the consuming\n * side will not use the last minute of a token's life.\n */\nexport const CHAT_TOKEN_REFRESH_MARGIN_SECONDS = 60;\n\n/**\n * How often the live socket re-authenticates, in seconds.\n *\n * Auth is an EVENT on the open socket rather than a handshake parameter, so\n * re-authenticating does not drop the connection.\n *\n * ⚠️ THIS MUST STAY BELOW THE REAL TOKEN LIFETIME, by more than the refresh\n * margin. It assumes a 15 minute token. A re-auth interval longer than the\n * lifetime is a socket that dies every quarter of an hour with no explanation,\n * and the symptom appears far from the cause. If the issued lifetime changes,\n * this number moves with it or the two silently disagree.\n */\nexport const CHAT_SOCKET_REAUTH_SECONDS = 10 * 60;\n\n/**\n * A short-lived token for the shared chat server.\n *\n * ⚠️ BOTH TIME FIELDS ARE KEPT, and they are not redundant, because two\n * different parties read them with two different clocks:\n *\n * `expiresAt` an absolute instant, for the SERVER. The chat server holds the\n * authoritative clock and must reject an expired token on its\n * own, whatever any client believes.\n * `expiresIn` a duration in seconds, for the CLIENT. Scheduling a refresh\n * from `receivedAt + expiresIn` uses only the client's own\n * elapsed time, so a device whose absolute clock is wrong still\n * refreshes correctly. Scheduling from `expiresAt` instead means\n * a browser an hour fast treats every fresh token as already\n * expired and re-mints forever.\n *\n * Dropping either one breaks the party that reads it. This is the same reason\n * OAuth carries `expires_in` on the wire while the token itself carries `exp`.\n *\n * ⚠️ AND THE CLIENT IS NOT THE ENFORCER. Its margin is an optimisation to avoid\n * a pointless round trip. A wrong clock, a tab asleep for an hour, or a modified\n * client all defeat anything the client does, so the chat server rejects an\n * expired token on its own.\n */\nexport const chatAuthTokenSchema = z.object({\n token: z.string().min(1),\n /** ISO 8601. Parsed with `Date.parse`, never an epoch number. */\n expiresAt: z.iso.datetime(),\n /** Seconds from issue. See the note above on why both exist. */\n expiresIn: z.number().int().positive(),\n /** The product this token is scoped to. A mismatch must fail loudly. */\n productId: chatProductIdSchema,\n});\nexport type ChatAuthToken = z.infer<typeof chatAuthTokenSchema>;\n\n// ─── Opening a conversation ──────────────────────────────────────────────────\n\n/**\n * `POST /api/v1/chat/open`: start (or find) the direct conversation with\n * another member.\n *\n * Our backend decides who may talk to whom, because only it knows this\n * product's rules, and it does the whole exchange with the chat server itself:\n * it mints the viewer's token and an invitation naming both participants, and\n * redeems them server to server. No credential reaches the browser.\n */\nexport const openChatBodySchema = z.object({\n /** The OTHER participant. The viewer is taken from the credential, never the body. */\n userId: objectIdSchema,\n});\nexport type OpenChatBody = z.infer<typeof openChatBodySchema>;\n\n/**\n * The conversation to navigate to. `201` with `wasCreated: true` the first\n * time, `200` and `false` on every later open of the same pair.\n */\nexport const openChatResponseSchema = z.object({\n conversationId: z.string().min(1),\n wasCreated: z.boolean(),\n});\nexport type OpenChatResponse = z.infer<typeof openChatResponseSchema>;\n\n// ─── Participants ────────────────────────────────────────────────────────────\n\n/**\n * `POST /api/v1/chat/participants`: names and avatars for the people in the\n * viewer's conversations.\n *\n * The chat server knows ids and nothing about who anybody is, because it serves\n * several products and identity belongs to each; so the conversation list asks\n * here, once per screen. Bounded by `CHAT_PARTICIPANTS_MAX`, so it cannot page\n * through the directory.\n */\nexport const chatParticipantsBodySchema = z.object({\n userIds: z.array(objectIdSchema).min(1).max(CHAT_PARTICIPANTS_MAX),\n});\nexport type ChatParticipantsBody = z.infer<typeof chatParticipantsBodySchema>;\n\n/** The public card fields any member already sees, and nothing more. */\nexport const chatParticipantSchema = z.object({\n displayName: z.string(),\n username: usernameSchema,\n /** Null when the member has no photo. Ours, or from the login provider. */\n avatarUrl: z.url().nullable(),\n});\nexport type ChatParticipant = z.infer<typeof chatParticipantSchema>;\n\n/**\n * Keyed by user id. ⚠ An id with no active, onboarded profile is ABSENT, not\n * an error: the client renders it as a former member rather than guessing.\n */\nexport const chatParticipantsResponseSchema = z.object({\n participants: z.record(z.string(), chatParticipantSchema),\n});\nexport type ChatParticipantsResponse = z.infer<typeof chatParticipantsResponseSchema>;\n\n// ─── Deprecated ──────────────────────────────────────────────────────────────\n//\n// Unused by every consumer, and still exported: removing an export trips the\n// publish rule that refuses a release missing anything the live one has, so it\n// is the user's decision, not a side effect. They were meant to go in 0.3.0;\n// that release kept them.\n\n/**\n * @deprecated Since 0.2.0: use `openChatBodySchema`, the same shape under the\n * name of the route that takes it.\n */\nexport const chatInvitationBodySchema = openChatBodySchema;\n/** @deprecated Since 0.2.0: use `OpenChatBody`. */\nexport type ChatInvitationBody = OpenChatBody;\n\n/**\n * @deprecated Since 0.2.0, and it describes nothing that exists. The browser\n * never holds an invitation: `/chat/open` mints and redeems it server to\n * server. The old `/invitation` route that handed one out could never have\n * worked and is gone.\n */\nexport const chatInvitationSchema = z.object({\n invitationToken: z.string().min(1),\n});\n/** @deprecated Since 0.2.0. */\nexport type ChatInvitation = z.infer<typeof chatInvitationSchema>;\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema: the staff Overview
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/dashboard the numbers a superadmin or manager opens the day with
|
|
5
|
+
*
|
|
6
|
+
* Counts only, computed by the server when asked: no member's data, so the
|
|
7
|
+
* screen can be glanced at over a shoulder. Each block answers one question:
|
|
8
|
+
* is the community growing, is it talking, is anything waiting on staff.
|
|
9
|
+
*
|
|
10
|
+
* "Last 7 days" is the seven days up to `generatedAt`, by the server's clock.
|
|
11
|
+
*
|
|
12
|
+
* @module founder-schema/domains/dashboard
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
export declare const dashboardSchema: z.ZodObject<{
|
|
16
|
+
members: z.ZodObject<{
|
|
17
|
+
total: z.ZodNumber;
|
|
18
|
+
joinedLast7Days: z.ZodNumber;
|
|
19
|
+
activeLast7Days: z.ZodNumber;
|
|
20
|
+
suspended: z.ZodNumber;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
content: z.ZodObject<{
|
|
23
|
+
postsLast7Days: z.ZodNumber;
|
|
24
|
+
pollsLast7Days: z.ZodNumber;
|
|
25
|
+
commentsLast7Days: z.ZodNumber;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
moderation: z.ZodObject<{
|
|
28
|
+
openReports: z.ZodNumber;
|
|
29
|
+
reportedItems: z.ZodNumber;
|
|
30
|
+
hiddenItems: z.ZodNumber;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
support: z.ZodObject<{
|
|
33
|
+
openTickets: z.ZodNumber;
|
|
34
|
+
unassignedTickets: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
team: z.ZodObject<{
|
|
37
|
+
staff: z.ZodNumber;
|
|
38
|
+
byRole: z.ZodRecord<z.ZodEnum<{
|
|
39
|
+
superadmin: "superadmin";
|
|
40
|
+
manager: "manager";
|
|
41
|
+
moderator: "moderator";
|
|
42
|
+
support_agent: "support_agent";
|
|
43
|
+
}>, z.ZodNumber>;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
generatedAt: z.ZodISODateTime;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export type Dashboard = z.infer<typeof dashboardSchema>;
|
|
48
|
+
//# sourceMappingURL=dashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../src/domains/dashboard/dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwC1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema: the staff Overview
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/dashboard the numbers a superadmin or manager opens the day with
|
|
5
|
+
*
|
|
6
|
+
* Counts only, computed by the server when asked: no member's data, so the
|
|
7
|
+
* screen can be glanced at over a shoulder. Each block answers one question:
|
|
8
|
+
* is the community growing, is it talking, is anything waiting on staff.
|
|
9
|
+
*
|
|
10
|
+
* "Last 7 days" is the seven days up to `generatedAt`, by the server's clock.
|
|
11
|
+
*
|
|
12
|
+
* @module founder-schema/domains/dashboard
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { platformRoleSchema } from '../../access/roles.js';
|
|
16
|
+
const count = z.number().int().min(0);
|
|
17
|
+
export const dashboardSchema = z.object({
|
|
18
|
+
members: z.object({
|
|
19
|
+
/** Accounts that are not deleted, suspended ones included. */
|
|
20
|
+
total: count,
|
|
21
|
+
joinedLast7Days: count,
|
|
22
|
+
/** Signed in and used the product at least once in the window. */
|
|
23
|
+
activeLast7Days: count,
|
|
24
|
+
suspended: count,
|
|
25
|
+
}),
|
|
26
|
+
content: z.object({
|
|
27
|
+
postsLast7Days: count,
|
|
28
|
+
pollsLast7Days: count,
|
|
29
|
+
/** Comments still up (not deleted, not hidden), like the posts and polls beside them. */
|
|
30
|
+
commentsLast7Days: count,
|
|
31
|
+
}),
|
|
32
|
+
moderation: z.object({
|
|
33
|
+
/** Reports nobody has decided on yet. */
|
|
34
|
+
openReports: count,
|
|
35
|
+
/** Posts, polls and comments those open reports are about. */
|
|
36
|
+
reportedItems: count,
|
|
37
|
+
/** Posts, polls and comments hidden by staff, not yet restored or removed. */
|
|
38
|
+
hiddenItems: count,
|
|
39
|
+
}),
|
|
40
|
+
support: z.object({
|
|
41
|
+
/** Tickets not resolved, waiting on an agent or on the member. */
|
|
42
|
+
openTickets: count,
|
|
43
|
+
/** Of those, the ones no agent has picked up. */
|
|
44
|
+
unassignedTickets: count,
|
|
45
|
+
}),
|
|
46
|
+
team: z.object({
|
|
47
|
+
/** People holding at least one staff role. */
|
|
48
|
+
staff: count,
|
|
49
|
+
/**
|
|
50
|
+
* People per role. Someone holding two roles counts once in each. Every
|
|
51
|
+
* role is a key (a record over an enum is exhaustive), zero rather than
|
|
52
|
+
* missing.
|
|
53
|
+
*/
|
|
54
|
+
byRole: z.record(platformRoleSchema, count),
|
|
55
|
+
}),
|
|
56
|
+
generatedAt: z.iso.datetime(),
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../../src/domains/dashboard/dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAEtC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;QAChB,8DAA8D;QAC9D,KAAK,EAAE,KAAK;QACZ,eAAe,EAAE,KAAK;QACtB,kEAAkE;QAClE,eAAe,EAAE,KAAK;QACtB,SAAS,EAAE,KAAK;KACjB,CAAC;IACF,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;QAChB,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,KAAK;QACrB,yFAAyF;QACzF,iBAAiB,EAAE,KAAK;KACzB,CAAC;IACF,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC;QACnB,yCAAyC;QACzC,WAAW,EAAE,KAAK;QAClB,8DAA8D;QAC9D,aAAa,EAAE,KAAK;QACpB,8EAA8E;QAC9E,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;QAChB,kEAAkE;QAClE,WAAW,EAAE,KAAK;QAClB,iDAAiD;QACjD,iBAAiB,EAAE,KAAK;KACzB,CAAC;IACF,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,8CAA8C;QAC9C,KAAK,EAAE,KAAK;QACZ;;;;WAIG;QACH,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,kBAAkB,EAAE,KAAK,CAAC;KAC5C,CAAC;IACF,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC","sourcesContent":["/**\n * founder-schema: the staff Overview\n *\n * GET /api/v1/dashboard the numbers a superadmin or manager opens the day with\n *\n * Counts only, computed by the server when asked: no member's data, so the\n * screen can be glanced at over a shoulder. Each block answers one question:\n * is the community growing, is it talking, is anything waiting on staff.\n *\n * \"Last 7 days\" is the seven days up to `generatedAt`, by the server's clock.\n *\n * @module founder-schema/domains/dashboard\n */\n\nimport { z } from 'zod';\nimport { platformRoleSchema } from '../../access/roles.js';\n\nconst count = z.number().int().min(0);\n\nexport const dashboardSchema = z.object({\n members: z.object({\n /** Accounts that are not deleted, suspended ones included. */\n total: count,\n joinedLast7Days: count,\n /** Signed in and used the product at least once in the window. */\n activeLast7Days: count,\n suspended: count,\n }),\n content: z.object({\n postsLast7Days: count,\n pollsLast7Days: count,\n /** Comments still up (not deleted, not hidden), like the posts and polls beside them. */\n commentsLast7Days: count,\n }),\n moderation: z.object({\n /** Reports nobody has decided on yet. */\n openReports: count,\n /** Posts, polls and comments those open reports are about. */\n reportedItems: count,\n /** Posts, polls and comments hidden by staff, not yet restored or removed. */\n hiddenItems: count,\n }),\n support: z.object({\n /** Tickets not resolved, waiting on an agent or on the member. */\n openTickets: count,\n /** Of those, the ones no agent has picked up. */\n unassignedTickets: count,\n }),\n team: z.object({\n /** People holding at least one staff role. */\n staff: count,\n /**\n * People per role. Someone holding two roles counts once in each. Every\n * role is a key (a record over an enum is exhaustive), zero rather than\n * missing.\n */\n byRole: z.record(platformRoleSchema, count),\n }),\n generatedAt: z.iso.datetime(),\n});\nexport type Dashboard = z.infer<typeof dashboardSchema>;\n"]}
|