@oxyhq/contracts 0.21.0 → 0.22.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.
@@ -1,11 +1,71 @@
1
1
  /**
2
- * Account graph wire contracts — organization taxonomy and create-account input.
2
+ * Account graph wire contracts — the account-kind vocabulary, organization
3
+ * taxonomy, and create-account input.
3
4
  *
4
5
  * `organizationCategory` classifies `kind: 'organization'` accounts (agency,
5
6
  * cooperative, landlord, …) without polluting `User.kind`. Meaningful only when
6
7
  * `kind === 'organization'`.
7
8
  */
8
9
  import { z } from 'zod';
10
+ /**
11
+ * The union is spelled out above and the array proves coverage BOTH ways
12
+ * (`satisfies` here, the `Gap` alias below) — the same shape this package's
13
+ * `ORGANIZATION_CATEGORIES` / `TRUST_TIERS` pairs use, and the one
14
+ * `db/schema/users.ts` mirrors to keep the `users_kind_check` CHECK honest.
15
+ *
16
+ * Deriving the union from the array instead would cost nothing here and be paid
17
+ * by consumers: `kind` travels into `@oxyhq/services` through
18
+ * `SwitchableAccount`, where an indexed-access type is materially more
19
+ * expensive to check than a literal union.
20
+ */
21
+ export const ACCOUNT_KINDS = [
22
+ 'personal',
23
+ 'organization',
24
+ 'project',
25
+ 'bot',
26
+ 'channel',
27
+ ];
28
+ export const accountKindSchema = z.enum(ACCOUNT_KINDS);
29
+ export const CHILD_ACCOUNT_KINDS = [
30
+ 'organization',
31
+ 'project',
32
+ 'bot',
33
+ 'channel',
34
+ ];
35
+ export const childAccountKindSchema = z.enum(CHILD_ACCOUNT_KINDS);
36
+ /**
37
+ * Whether an operator may ACT AS an account of this kind — switch the whole app
38
+ * into it (`POST /accounts/:id/switch`) or authorise an app to act as it
39
+ * (an OAuth delegated subject).
40
+ *
41
+ * Two kinds are refused, for opposite reasons:
42
+ *
43
+ * - `personal` is a human login, so assuming it would be impersonation.
44
+ * - `channel` is a CONTENT identity, not an operating one. A channel exists so
45
+ * that posts can be authored BY it; it is never a seat anybody occupies. Its
46
+ * operators act on it through their own membership, and an application
47
+ * publishes to it with its own credential. Refusing act-as is what makes
48
+ * "no login, ever" structural rather than incidental: no session can be
49
+ * minted whose subject is a channel, so no bearer exists that could add an
50
+ * auth method to one (every auth-method write resolves its target from the
51
+ * authenticated subject, never from a parameter).
52
+ *
53
+ * Consumers must gate on this predicate rather than testing `kind === 'personal'`,
54
+ * which silently admits every kind added after it was written.
55
+ */
56
+ export function isActAsEligibleKind(kind) {
57
+ return kind === 'organization' || kind === 'project' || kind === 'bot';
58
+ }
59
+ /**
60
+ * Narrow an unknown value to an {@link AccountKind}.
61
+ *
62
+ * The user-DTO serializers read from structurally-permissive `unknown` sources
63
+ * (a Drizzle row, a Mongo document, an already-formatted object), so each one
64
+ * would otherwise hand-roll this check and they would drift on what counts.
65
+ */
66
+ export function isAccountKind(value) {
67
+ return typeof value === 'string' && ACCOUNT_KINDS.includes(value);
68
+ }
9
69
  export const ORGANIZATION_CATEGORIES = [
10
70
  'agency',
11
71
  'cooperative',
@@ -13,10 +73,25 @@ export const ORGANIZATION_CATEGORIES = [
13
73
  'other',
14
74
  ];
15
75
  export const organizationCategorySchema = z.enum(ORGANIZATION_CATEGORIES);
76
+ /**
77
+ * An account's name on the create/update wire.
78
+ *
79
+ * `displayName` is EXPLICIT and stored, not derived. `first`/`last` model a
80
+ * human name, and composing a display string from them is right for a person —
81
+ * but a non-personal account has a TITLE, not a given and family name. Without
82
+ * this field the only way to name a channel "Notas de Nate" was to put the whole
83
+ * title in `first`, which renders correctly by accident while recording it as
84
+ * somebody's given name.
85
+ *
86
+ * When present it wins over the composed `first`/`last` (see the API's
87
+ * `composeDisplayName`, which already preferred an explicit value — only the
88
+ * storage for one was missing).
89
+ */
16
90
  const accountNameSchema = z
17
91
  .object({
18
92
  first: z.string().trim().max(100).optional(),
19
93
  last: z.string().trim().max(100).optional(),
94
+ displayName: z.string().trim().max(100).optional(),
20
95
  })
21
96
  .optional();
22
97
  /**
@@ -26,7 +101,7 @@ const accountNameSchema = z
26
101
  export const createAccountRequestSchema = z
27
102
  .object({
28
103
  parentAccountId: z.string().trim().min(1).optional(),
29
- kind: z.enum(['organization', 'project', 'bot']),
104
+ kind: childAccountKindSchema,
30
105
  username: z.string().trim().min(1).max(100),
31
106
  name: accountNameSchema,
32
107
  bio: z.string().trim().max(500).optional(),
package/dist/esm/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * Platform-agnostic — zod is the only runtime dependency. No react/react-native/
10
10
  * expo, no `require()` in the ESM build.
11
11
  */
12
- export { ORGANIZATION_CATEGORIES, organizationCategorySchema, createAccountRequestSchema, } from './accountGraph.js';
12
+ export { ACCOUNT_KINDS, accountKindSchema, CHILD_ACCOUNT_KINDS, childAccountKindSchema, isAccountKind, isActAsEligibleKind, ORGANIZATION_CATEGORIES, organizationCategorySchema, createAccountRequestSchema, } from './accountGraph.js';
13
13
  export {
14
14
  // Schemas
15
15
  userNameSchema, userRelationshipSchema, themePreferenceSchema, userResponseSchema, userProfileUpdateSchema, currentUserResponseSchema, deviceLinkedSessionSchema, deviceLinkedSessionsResponseSchema,
@@ -30,7 +30,7 @@
30
30
  */
31
31
  import { z } from 'zod';
32
32
  import { verifiedDomainSchema } from './identity.js';
33
- import { organizationCategorySchema } from './accountGraph.js';
33
+ import { accountKindSchema, organizationCategorySchema } from './accountGraph.js';
34
34
  export const userNameSchema = z
35
35
  .object({
36
36
  first: z.string().optional(),
@@ -100,9 +100,23 @@ export const userResponseSchema = z
100
100
  * entry; present only when the account has verified at least one domain.
101
101
  */
102
102
  verifiedDomains: z.array(verifiedDomainSchema).optional(),
103
+ /**
104
+ * Account-graph classification — what KIND of account this is.
105
+ *
106
+ * ORTHOGONAL to `type` (`local` / `federated` / `agent` / `automated`),
107
+ * which says where the account lives and how it is driven; the two
108
+ * coexist and neither substitutes for the other. A `channel` is a
109
+ * publishing identity nobody can act as, so a consumer that renders
110
+ * authored content reads THIS to tell a channel's post from a person's.
111
+ *
112
+ * Optional because a DTO produced from a source that never carried the
113
+ * column omits it; absent should be read as `personal`, the column's
114
+ * default, not as unknown.
115
+ */
116
+ kind: accountKindSchema.optional(),
103
117
  /**
104
118
  * Real-estate / team taxonomy for `kind: 'organization'` accounts.
105
- * Absent on personal, project, and bot accounts.
119
+ * Absent on personal, project, bot, and channel accounts.
106
120
  */
107
121
  organizationCategory: organizationCategorySchema.optional(),
108
122
  /**
@@ -126,6 +140,12 @@ export const userProfileUpdateSchema = z
126
140
  .object({
127
141
  first: z.string().optional(),
128
142
  last: z.string().optional(),
143
+ /**
144
+ * Explicit display name, stored rather than composed. Wins over
145
+ * `first`/`last` when set; send `''` to clear it and fall back
146
+ * to the composed pair.
147
+ */
148
+ displayName: z.string().optional(),
129
149
  })
130
150
  .optional(),
131
151
  username: z.string().optional(),