@heychunky/core 0.15.1 → 0.16.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.
@@ -31,15 +31,26 @@ export interface App {
31
31
  /** What the run did, step by step. Null for an app recorded before this. */
32
32
  provisioned: StepResult[] | null;
33
33
  }
34
- /** Refused because the owner already has as many apps as they may. */
34
+ /** Refused because the organisation already has as many apps as it may. */
35
35
  export declare class AppLimitError extends Error {
36
36
  readonly limit: number;
37
- constructor(email: string, limit: number);
37
+ constructor(org: string, limit: number);
38
38
  }
39
39
  export interface AppRecord {
40
40
  name: string;
41
+ /** The organisation that owns it. The tenancy boundary. */
42
+ orgId: string;
43
+ /** Who created it. Provenance, not tenancy. */
41
44
  owner: string;
42
- org?: string | undefined;
45
+ /**
46
+ * The GitHub organisation the repository sits in.
47
+ *
48
+ * Named apart from `orgId` on purpose. Both used to be called `org`, one
49
+ * meaning a tenant here and one meaning an account at a provider, and two
50
+ * fields a keystroke apart with different blast radii is how somebody
51
+ * eventually writes a tenant id into a repository path.
52
+ */
53
+ githubOrg?: string | undefined;
43
54
  repo?: string | undefined;
44
55
  vercelProject?: string | undefined;
45
56
  neonProject?: string | undefined;
@@ -54,23 +65,31 @@ export interface AppRecord {
54
65
  /** The user id for an email, or the sole superadmin when none is named. */
55
66
  export declare function ownerId(email: string | undefined, register: Register): Promise<string>;
56
67
  /**
57
- * Refuse before anything is created, if the owner is at their limit.
68
+ * Refuse before anything is created, if the organisation is at its limit.
58
69
  *
59
70
  * Asked before provisioning rather than at the point of recording, because by
60
71
  * then a repository, a database and a deployment target already exist and the
61
72
  * limit has been passed rather than enforced.
62
73
  *
63
- * A count of what someone already has, against a number on their row. It is
64
- * not billing it is the thing that makes inviting someone a bounded
74
+ * Counted per organisation, because that is what pays. A ceiling on the person
75
+ * would let one team of five have five times what it bought by taking turns
76
+ * creating things, and would charge a solo developer for having two
77
+ * organisations.
78
+ *
79
+ * It is not billing — it is the thing that makes inviting someone a bounded
65
80
  * decision, which is what lets invitations be given out at all.
66
81
  */
67
- export declare function assertCanCreate(owner: string, register: Register): Promise<void>;
82
+ export declare function assertCanCreate(orgId: string, register: Register): Promise<void>;
68
83
  /**
69
84
  * Record an app, or update what is known about one.
70
85
  *
71
- * By name, because that is what is unique across the factory a repository, a
72
- * Vercel project and a database all carry it. Re-running provisioning fills in
73
- * what a first, partial run could not, rather than making a second row.
86
+ * By organisation and name, which is what is unique now that two tenants may
87
+ * each have a `blog`. Re-running provisioning fills in what a first, partial
88
+ * run could not, rather than making a second row.
89
+ *
90
+ * Provider names are still global and are not what this conflicts on: a
91
+ * repository and a Vercel project cannot collide, and provisioning is what has
92
+ * to answer for that rather than the register.
74
93
  */
75
94
  export declare function recordApp(app: AppRecord, register: Register): Promise<App>;
76
95
  /**
@@ -105,8 +124,11 @@ export declare class NothingToAdoptError extends Error {
105
124
  */
106
125
  export declare function adoptApp(steps: readonly StepResult[], app: {
107
126
  name: string;
127
+ /** The organisation to adopt it into. */
128
+ orgId: string;
108
129
  owner: string;
109
- org?: string | undefined;
130
+ /** The GitHub organisation its repository sits in. Not the tenant. */
131
+ githubOrg?: string | undefined;
110
132
  /**
111
133
  * A Neon project the walk cannot find by name.
112
134
  *
@@ -120,26 +142,29 @@ export declare function adoptApp(steps: readonly StepResult[], app: {
120
142
  */
121
143
  neonProject?: string | undefined;
122
144
  }, register: Register): Promise<App>;
123
- /** Apps an owner has, or every app when none is named. */
124
145
  /**
125
- * One app, if this operator may have it.
146
+ * Refused because a developer asked a tenancy question without naming a tenant.
147
+ *
148
+ * Not a permission answer — a bug. Returning "nothing" would look exactly like
149
+ * an empty organisation, and a caller that forgot to say which organisation it
150
+ * meant would ship looking like it worked.
151
+ */
152
+ export declare class NoOrgError extends Error {
153
+ constructor();
154
+ }
155
+ /**
156
+ * Every app in one organisation, if this person is in it.
126
157
  *
127
- * The tenancy rule asked of a single row: a developer reaching for somebody
128
- * else's app by name gets nothing, which is the same answer as an app that
129
- * does not exist. Telling those apart would confirm what other people have
130
- * named their apps, and the caller is expected to answer both the same way.
158
+ * Ordered oldest first, which is the order somebody built them in and the only
159
+ * order that does not move under them as they work.
160
+ */
161
+ export declare function appsFor(user: Pick<User, "id" | "role">, org: string | undefined, register: Register): Promise<App[]>;
162
+ /**
163
+ * One app, if this person may have it through this organisation.
131
164
  *
132
165
  * This is what replaces a configured target repository. A run does not hold a
133
- * repo; it resolves one — from who is asking and which app they named — and
134
- * `repo` on the row it returns is the answer. Which means the authorisation
135
- * and the lookup are the same call, and a caller cannot perform the second
136
- * while forgetting the first.
137
- *
138
- * In the Core rather than in a caller because there are now three: the pages,
139
- * the API, and the superagent working on somebody's app on their behalf. A
140
- * tenancy rule that lives in one of them is a rule the other two can differ
141
- * from.
166
+ * repo; it resolves one — from who is asking, which organisation they are in,
167
+ * and which app they named — and `repo` on the row it returns is the answer.
142
168
  */
143
- export declare function appFor(user: Pick<User, "id" | "role">, name: string, register: Register): Promise<App | undefined>;
144
- export declare function listApps(owner: string | undefined, register: Register): Promise<App[]>;
169
+ export declare function appFor(user: Pick<User, "id" | "role">, org: string | undefined, name: string, register: Register): Promise<App | undefined>;
145
170
  //# sourceMappingURL=apps.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../../src/admin/apps.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../../src/admin/apps.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
- /** Refused because the owner already has as many apps as they may. */
1
+ /** Refused because the organisation already has as many apps as it may. */
2
2
  export class AppLimitError extends Error {
3
3
  limit;
4
- constructor(email, limit) {
5
- super(`${email} already has ${limit} app${limit === 1 ? "" : "s"}, which is their limit.`);
4
+ constructor(org, limit) {
5
+ super(`${org} already has ${limit} app${limit === 1 ? "" : "s"}, which is its limit.`);
6
6
  this.name = "AppLimitError";
7
7
  this.limit = limit;
8
8
  }
@@ -26,24 +26,27 @@ export async function ownerId(email, register) {
26
26
  return String(id);
27
27
  }
28
28
  /**
29
- * Refuse before anything is created, if the owner is at their limit.
29
+ * Refuse before anything is created, if the organisation is at its limit.
30
30
  *
31
31
  * Asked before provisioning rather than at the point of recording, because by
32
32
  * then a repository, a database and a deployment target already exist and the
33
33
  * limit has been passed rather than enforced.
34
34
  *
35
- * A count of what someone already has, against a number on their row. It is
36
- * not billing it is the thing that makes inviting someone a bounded
35
+ * Counted per organisation, because that is what pays. A ceiling on the person
36
+ * would let one team of five have five times what it bought by taking turns
37
+ * creating things, and would charge a solo developer for having two
38
+ * organisations.
39
+ *
40
+ * It is not billing — it is the thing that makes inviting someone a bounded
37
41
  * decision, which is what lets invitations be given out at all.
38
42
  */
39
- export async function assertCanCreate(owner, register) {
40
- const conn = register;
41
- const { rows } = await conn.query(`select u.email, u.app_limit,
42
- (select count(*) from apps a where a.owner_id = u.id) as owned
43
- from users u where u.id = $1`, [owner]);
43
+ export async function assertCanCreate(orgId, register) {
44
+ const { rows } = await register.query(`select o.name, o.app_limit,
45
+ (select count(*) from apps a where a.org_id = o.id) as owned
46
+ from orgs o where o.id = $1`, [orgId]);
44
47
  const row = rows[0];
45
48
  if (!row)
46
- throw new Error("No such owner.");
49
+ throw new Error("No such organisation.");
47
50
  // No limit recorded means no ceiling, not a ceiling of zero. Number(null) is
48
51
  // 0 and `owned >= 0` is always true, so reading it the other way locks
49
52
  // somebody out of their own account — and does it silently, at the moment
@@ -55,40 +58,42 @@ export async function assertCanCreate(owner, register) {
55
58
  if (!Number.isFinite(limit))
56
59
  return;
57
60
  if (Number(row["owned"]) >= limit)
58
- throw new AppLimitError(String(row["email"]), limit);
61
+ throw new AppLimitError(String(row["name"]), limit);
59
62
  }
60
63
  /**
61
64
  * Record an app, or update what is known about one.
62
65
  *
63
- * By name, because that is what is unique across the factory a repository, a
64
- * Vercel project and a database all carry it. Re-running provisioning fills in
65
- * what a first, partial run could not, rather than making a second row.
66
+ * By organisation and name, which is what is unique now that two tenants may
67
+ * each have a `blog`. Re-running provisioning fills in what a first, partial
68
+ * run could not, rather than making a second row.
69
+ *
70
+ * Provider names are still global and are not what this conflicts on: a
71
+ * repository and a Vercel project cannot collide, and provisioning is what has
72
+ * to answer for that rather than the register.
66
73
  */
67
74
  export async function recordApp(app, register) {
68
75
  const conn = register;
69
76
  const { rows } = await conn.write(
70
- // `org_id` is derived rather than passed, and derived the same way the
71
- // backfill derived it: through the owner's username to their personal
72
- // organisation. A caller that had to supply it is a caller that can supply
73
- // the wrong one, and the invariant `heychunky register verify` asserts
74
- // every app is in an organisation would then hold only until the next
75
- // app was made.
77
+ // `org_id` is passed now rather than derived from the owner's username.
78
+ // While every organisation was somebody's personal one, deriving it was the
79
+ // safer shape nothing could supply the wrong tenant. A team has no such
80
+ // derivation: the same person can create in two organisations, and only the
81
+ // caller knows which one they were acting in.
82
+ //
83
+ // The column is `not null`, so an app that names no organisation is refused
84
+ // by the database rather than becoming a row nobody can see.
76
85
  //
77
86
  // Kept on conflict, never overwritten. Re-running provisioning fills in
78
87
  // what a partial run could not; it must not move an app between tenants.
79
88
  `insert into apps (name, owner_id, org_id, org, repo, vercel_project, neon_project, provisioned)
80
- values ($1, $2,
81
- (select o.id from orgs o join users u on lower(o.name) = lower(u.username)
82
- where u.id = $2 and o.kind = 'personal'),
83
- $3, $4, $5, $6, $7)
84
- on conflict (lower(name)) do update set
85
- org_id = coalesce(apps.org_id, excluded.org_id),
89
+ values ($1, $2, $3, $4, $5, $6, $7, $8)
90
+ on conflict (org_id, lower(name)) do update set
86
91
  org = coalesce(excluded.org, apps.org),
87
92
  repo = coalesce(excluded.repo, apps.repo),
88
93
  vercel_project = coalesce(excluded.vercel_project, apps.vercel_project),
89
94
  neon_project = coalesce(excluded.neon_project, apps.neon_project),
90
95
  provisioned = coalesce(excluded.provisioned, apps.provisioned)
91
- returning *`, [app.name, app.owner, app.org ?? null, app.repo ?? null,
96
+ returning *`, [app.name, app.owner, app.orgId, app.githubOrg ?? null, app.repo ?? null,
92
97
  app.vercelProject ?? null, app.neonProject ?? null,
93
98
  app.provisioned ? JSON.stringify(app.provisioned) : null]);
94
99
  return rows[0];
@@ -140,48 +145,85 @@ export async function adoptApp(steps, app, register) {
140
145
  const found = detailsFromRun(steps);
141
146
  return recordApp({
142
147
  name: app.name,
148
+ orgId: app.orgId,
143
149
  owner: app.owner,
144
- org: app.org,
150
+ githubOrg: app.githubOrg,
145
151
  ...found,
146
152
  neonProject: app.neonProject ?? found.neonProject,
147
153
  }, register);
148
154
  }
149
- /** Apps an owner has, or every app when none is named. */
150
155
  /**
151
- * One app, if this operator may have it.
156
+ * Refused because a developer asked a tenancy question without naming a tenant.
157
+ *
158
+ * Not a permission answer — a bug. Returning "nothing" would look exactly like
159
+ * an empty organisation, and a caller that forgot to say which organisation it
160
+ * meant would ship looking like it worked.
161
+ */
162
+ export class NoOrgError extends Error {
163
+ constructor() {
164
+ super("Which organisation? Every question about apps is asked of one.");
165
+ this.name = "NoOrgError";
166
+ }
167
+ }
168
+ /**
169
+ * The apps clause, with the tenancy rule fused into it.
152
170
  *
153
- * The tenancy rule asked of a single row: a developer reaching for somebody
154
- * else's app by name gets nothing, which is the same answer as an app that
155
- * does not exist. Telling those apart would confirm what other people have
156
- * named their apps, and the caller is expected to answer both the same way.
171
+ * `org_id` is the tenancy boundary; `owner_id` survives beside it as
172
+ * provenance. So the question a developer is really asking is *am I in this
173
+ * organisation*, and that `exists` is the whole of the answer a person
174
+ * reaching into an organisation they are not in gets nothing, which is the same
175
+ * answer as an organisation that does not exist. Telling those apart would
176
+ * confirm what other people have named their organisations.
157
177
  *
158
- * This is what replaces a configured target repository. A run does not hold a
159
- * repo; it resolves one from who is asking and which app they named — and
160
- * `repo` on the row it returns is the answer. Which means the authorisation
161
- * and the lookup are the same call, and a caller cannot perform the second
162
- * while forgetting the first.
178
+ * A superadmin omits the membership clause and, with no organisation named,
179
+ * omits the scope entirely: that is the factory role, and `/admin` is the one
180
+ * screen that must see across tenants.
163
181
  *
164
- * In the Core rather than in a caller because there are now three: the pages,
165
- * the API, and the superagent working on somebody's app on their behalf. A
166
- * tenancy rule that lives in one of them is a rule the other two can differ
167
- * from.
182
+ * Built here rather than in each query so that authorisation and lookup are the
183
+ * same call and cannot come apart. There are four callers now the pages, the
184
+ * API, the CLI and the superagent and a tenancy rule written four times is
185
+ * one the four can differ on.
168
186
  */
169
- export async function appFor(user, name, register) {
170
- const select = `select a.*, u.email as owner_email from apps a
171
- join users u on u.id = a.owner_id
172
- where lower(a.name) = lower($1)`;
173
- const { rows } = user.role === "superadmin"
174
- ? await register.query(select, [name])
175
- : await register.query(`${select} and a.owner_id = $2`, [name, user.id]);
176
- return rows[0];
187
+ function scoped(user, org, where, params) {
188
+ const select = `select a.*, u.email as owner_email, o.name as org_name from apps a
189
+ join users u on u.id = a.owner_id
190
+ join orgs o on o.id = a.org_id`;
191
+ if (!org) {
192
+ if (user.role !== "superadmin")
193
+ throw new NoOrgError();
194
+ return { sql: `${select} ${where}`, params };
195
+ }
196
+ const orgAt = `$${params.length + 1}`;
197
+ const sql = `${select} ${where}${where.trim() ? " and" : "where"} lower(o.name) = lower(${orgAt})`;
198
+ if (user.role === "superadmin")
199
+ return { sql, params: [...params, org] };
200
+ return {
201
+ sql: `${sql} and exists (select 1 from org_members m
202
+ where m.org_id = o.id and m.user_id = $${params.length + 2})`,
203
+ params: [...params, org, user.id],
204
+ };
177
205
  }
178
- export async function listApps(owner, register) {
179
- const conn = register;
180
- const { rows } = owner
181
- ? await conn.query(`select a.*, u.email as owner_email from apps a join users u on u.id = a.owner_id
182
- where a.owner_id = $1 order by a.created_at`, [owner])
183
- : await conn.query(`select a.*, u.email as owner_email from apps a join users u on u.id = a.owner_id
184
- order by a.created_at`);
206
+ /**
207
+ * Every app in one organisation, if this person is in it.
208
+ *
209
+ * Ordered oldest first, which is the order somebody built them in and the only
210
+ * order that does not move under them as they work.
211
+ */
212
+ export async function appsFor(user, org, register) {
213
+ const { sql, params } = scoped(user, org, "", []);
214
+ const { rows } = await register.query(`${sql} order by a.created_at`, params);
185
215
  return rows;
186
216
  }
217
+ /**
218
+ * One app, if this person may have it through this organisation.
219
+ *
220
+ * This is what replaces a configured target repository. A run does not hold a
221
+ * repo; it resolves one — from who is asking, which organisation they are in,
222
+ * and which app they named — and `repo` on the row it returns is the answer.
223
+ */
224
+ export async function appFor(user, org, name, register) {
225
+ const { sql, params } = scoped(user, org, "where lower(a.name) = lower($1)", [name]);
226
+ const { rows } = await register.query(sql, params);
227
+ return rows[0];
228
+ }
187
229
  //# sourceMappingURL=apps.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"apps.js","sourceRoot":"","sources":["../../src/admin/apps.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apps.js","sourceRoot":"","sources":["../../src/admin/apps.ts"],"names":[],"mappings":"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"}
@@ -53,6 +53,16 @@ export type OrgKind = (typeof ORG_KINDS)[number];
53
53
  * only in TypeScript is a rule the next caller writes around.
54
54
  */
55
55
  export declare const ORG_NAME: RegExp;
56
+ /**
57
+ * Why this name cannot be an organisation, or undefined if it can.
58
+ *
59
+ * One rule, in the Core, because a name is three things at once: the publish
60
+ * scope, the URL segment, and — for a personal organisation — somebody's
61
+ * username. The app used to hold its own copy for usernames; two lists would
62
+ * disagree eventually, and the disagreement would be somebody claiming a name
63
+ * on one screen that means something else on another.
64
+ */
65
+ export declare function invalidOrgName(raw: string): string | undefined;
56
66
  export interface Org {
57
67
  id: string;
58
68
  /** The publish scope and the URL segment. Unique without case. */
@@ -60,6 +70,8 @@ export interface Org {
60
70
  kind: OrgKind;
61
71
  /** What this tenant may spend. The plan moved here from the user. */
62
72
  plan: PlanKey;
73
+ /** Bends the plan's ceiling for one tenant. Null is no ceiling, not zero. */
74
+ app_limit: number | null;
63
75
  /** A provider's own name for the same tenant. Null until one is linked. */
64
76
  github_org: string | null;
65
77
  vercel_team: string | null;
@@ -70,7 +82,13 @@ export interface Org {
70
82
  export interface Membership extends Org {
71
83
  role: OrgRole;
72
84
  }
73
- /** Refused because the name belongs to an organisation that is not personal. */
85
+ /**
86
+ * Refused because somebody already has the name.
87
+ *
88
+ * Deliberately says no more than that. Whether the holder is a person or a
89
+ * team is not the asker's business, and answering would turn this into a way
90
+ * to enumerate who exists.
91
+ */
74
92
  export declare class OrgNameTakenError extends Error {
75
93
  constructor(name: string);
76
94
  }
@@ -139,4 +157,31 @@ export interface CheckResult {
139
157
  * one to swallow, so nothing here catches.
140
158
  */
141
159
  export declare function verifyOrgs(register: Register): Promise<CheckResult[]>;
160
+ /**
161
+ * Make a team organisation, with its founder running it.
162
+ *
163
+ * The one namespace is what makes this safe to state simply: a personal
164
+ * organisation is created by claiming a username, so every username already has
165
+ * a row here, and `orgs_name_key` is what refuses a team that reaches for one.
166
+ * There is no second list to consult and no race to lose — the unique index
167
+ * settles it, the way it settles a username.
168
+ *
169
+ * The founder is an `admin` rather than an owner. Ownership is not a role: it
170
+ * is what `kind` and, for a personal organisation, the matching username
171
+ * already say. The two roles that exist describe what somebody may *do*.
172
+ *
173
+ * No plan is chosen here. A new organisation starts free and is put on a plan
174
+ * the way an account is — assigned, because it decides how much of the
175
+ * factory's money it may spend.
176
+ */
177
+ export declare function createOrg(name: string, founder: string, register: Register): Promise<Org>;
178
+ /**
179
+ * Somebody's personal organisation.
180
+ *
181
+ * The one everybody has from the moment they claim a username, and where the
182
+ * operator puts an app when nobody said otherwise. Undefined for a user who has
183
+ * not claimed a name yet — which is a real state, not an error: an account
184
+ * exists before its owner has chosen what to be called.
185
+ */
186
+ export declare function personalOrgFor(userId: string, register: Register): Promise<Org | undefined>;
142
187
  //# sourceMappingURL=orgs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/admin/orgs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,0EAA0E;AAC1E,eAAO,MAAM,SAAS,iCAAkC,CAAC;AACzD,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,+BAAgC,CAAC;AACvD,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,QAAyB,CAAC;AAE/C,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,qEAAqE;IACrE,IAAI,EAAE,OAAO,CAAC;IACd,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,WAAW,UAAW,SAAQ,GAAG;IACrC,IAAI,EAAE,OAAO,CAAC;CACf;AAED,gFAAgF;AAChF,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,IAAI,EAAE,MAAM;CAIzB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,GAAG,CAAC,CAwBd;AAED,oEAAoE;AACpE,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQvF;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAqD/D,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,OAAO,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAQ3E"}
1
+ {"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/admin/orgs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,0EAA0E;AAC1E,eAAO,MAAM,SAAS,iCAAkC,CAAC;AACzD,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,+BAAgC,CAAC;AACvD,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,QAA2C,CAAC;AAgBjE;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAW9D;AAED,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,qEAAqE;IACrE,IAAI,EAAE,OAAO,CAAC;IACd,6EAA6E;IAC7E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,WAAW,UAAW,SAAQ,GAAG;IACrC,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,IAAI,EAAE,MAAM;CAIzB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,GAAG,CAAC,CAyBd;AAED,oEAAoE;AACpE,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQvF;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAqD/D,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,OAAO,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAQ3E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,GAAG,CAAC,CAsBd;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC,CAO1B"}
@@ -15,11 +15,56 @@ export const ORG_KINDS = ["personal", "team"];
15
15
  * string. Enforced by a check constraint as well as here — a rule that lives
16
16
  * only in TypeScript is a rule the next caller writes around.
17
17
  */
18
- export const ORG_NAME = /^[a-z0-9][a-z0-9-]*$/;
19
- /** Refused because the name belongs to an organisation that is not personal. */
18
+ export const ORG_NAME = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
19
+ /**
20
+ * Names the product needs for itself, or that would read as someone official.
21
+ *
22
+ * Kept short on purpose: a long denylist is a list of names nobody can have for
23
+ * reasons nobody remembers. It matters more than it did — an organisation's
24
+ * name is a URL segment now, so `/@admin/apps` would be a page that looks like
25
+ * ours and is not.
26
+ */
27
+ const RESERVED = new Set([
28
+ "admin", "administrator", "root", "superadmin", "support", "help",
29
+ "chunky", "heychunky", "api", "app", "www", "registry", "r", "new",
30
+ "settings", "account", "billing", "login", "signin", "signout", "me",
31
+ ]);
32
+ /**
33
+ * Why this name cannot be an organisation, or undefined if it can.
34
+ *
35
+ * One rule, in the Core, because a name is three things at once: the publish
36
+ * scope, the URL segment, and — for a personal organisation — somebody's
37
+ * username. The app used to hold its own copy for usernames; two lists would
38
+ * disagree eventually, and the disagreement would be somebody claiming a name
39
+ * on one screen that means something else on another.
40
+ */
41
+ export function invalidOrgName(raw) {
42
+ const name = raw.trim();
43
+ if (!name)
44
+ return "Pick a name.";
45
+ if (name !== name.toLowerCase())
46
+ return "Names are lowercase.";
47
+ if (name.length < 2)
48
+ return "That is too short — two characters at least.";
49
+ if (name.length > 32)
50
+ return "That is too long — thirty-two characters at most.";
51
+ if (!ORG_NAME.test(name)) {
52
+ return "Letters, numbers and hyphens only, starting and ending with a letter or number.";
53
+ }
54
+ if (RESERVED.has(name))
55
+ return "That one is spoken for.";
56
+ return undefined;
57
+ }
58
+ /**
59
+ * Refused because somebody already has the name.
60
+ *
61
+ * Deliberately says no more than that. Whether the holder is a person or a
62
+ * team is not the asker's business, and answering would turn this into a way
63
+ * to enumerate who exists.
64
+ */
20
65
  export class OrgNameTakenError extends Error {
21
66
  constructor(name) {
22
- super(`"${name}" is a team organisation, so it cannot also be somebody's personal one.`);
67
+ super(`The name "${name}" is taken.`);
23
68
  this.name = "OrgNameTakenError";
24
69
  }
25
70
  }
@@ -44,8 +89,9 @@ export class OrgNameTakenError extends Error {
44
89
  * would hand them somebody else's tenant. So it is looked at, and refused.
45
90
  */
46
91
  export async function claimPersonalOrg(userId, username, register) {
47
- if (!ORG_NAME.test(username))
48
- throw new Error(`"${username}" is not a name an organisation can have.`);
92
+ const bad = invalidOrgName(username);
93
+ if (bad)
94
+ throw new Error(bad);
49
95
  await register.write(`insert into orgs (name, kind, plan)
50
96
  select $1, 'personal', coalesce(u.plan, 'free') from users u where u.id = $2
51
97
  on conflict (lower(name)) do nothing`, [username, userId]);
@@ -163,4 +209,51 @@ export async function verifyOrgs(register) {
163
209
  }
164
210
  return out;
165
211
  }
212
+ /**
213
+ * Make a team organisation, with its founder running it.
214
+ *
215
+ * The one namespace is what makes this safe to state simply: a personal
216
+ * organisation is created by claiming a username, so every username already has
217
+ * a row here, and `orgs_name_key` is what refuses a team that reaches for one.
218
+ * There is no second list to consult and no race to lose — the unique index
219
+ * settles it, the way it settles a username.
220
+ *
221
+ * The founder is an `admin` rather than an owner. Ownership is not a role: it
222
+ * is what `kind` and, for a personal organisation, the matching username
223
+ * already say. The two roles that exist describe what somebody may *do*.
224
+ *
225
+ * No plan is chosen here. A new organisation starts free and is put on a plan
226
+ * the way an account is — assigned, because it decides how much of the
227
+ * factory's money it may spend.
228
+ */
229
+ export async function createOrg(name, founder, register) {
230
+ const bad = invalidOrgName(name);
231
+ if (bad)
232
+ throw new Error(bad);
233
+ const { rows } = await register.write(`insert into orgs (name, kind, plan) values ($1, 'team', 'free')
234
+ on conflict (lower(name)) do nothing
235
+ returning *`, [name]);
236
+ const org = rows[0];
237
+ // `do nothing` returns no row when the name was taken, which is the whole
238
+ // answer: somebody has it, and whether they are a person or a team is not
239
+ // this caller's business to find out.
240
+ if (!org)
241
+ throw new OrgNameTakenError(name);
242
+ await register.write(`insert into org_members (org_id, user_id, role) values ($1, $2, 'admin')
243
+ on conflict (org_id, user_id) do nothing`, [org.id, founder]);
244
+ return org;
245
+ }
246
+ /**
247
+ * Somebody's personal organisation.
248
+ *
249
+ * The one everybody has from the moment they claim a username, and where the
250
+ * operator puts an app when nobody said otherwise. Undefined for a user who has
251
+ * not claimed a name yet — which is a real state, not an error: an account
252
+ * exists before its owner has chosen what to be called.
253
+ */
254
+ export async function personalOrgFor(userId, register) {
255
+ const { rows } = await register.query(`select o.* from orgs o join users u on lower(u.username) = lower(o.name)
256
+ where u.id = $1 and o.kind = 'personal'`, [userId]);
257
+ return rows[0];
258
+ }
166
259
  //# sourceMappingURL=orgs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"orgs.js","sourceRoot":"","sources":["../../src/admin/orgs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"orgs.js","sourceRoot":"","sources":["../../src/admin/orgs.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heychunky/core",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "The engine the Chunky CLI, worker and apps run on.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,6 @@
1
+ -- The per-account override follows the plan onto the organisation.
2
+ --
3
+ -- `users.app_limit` was "bend the ceiling for one person without inventing a
4
+ -- sixth plan". Now that apps are counted per organisation, an override that
5
+ -- stayed on the user would bend a ceiling nobody measures against.
6
+ alter table orgs add column app_limit integer;
@@ -0,0 +1 @@
1
+ alter table orgs add constraint orgs_app_limit_check check ((app_limit >= 0));
@@ -0,0 +1,8 @@
1
+ -- Personal organisations inherit their person's override, so nobody's ceiling
2
+ -- moves on the day the reads do. `users.app_limit` is dead after this and is
3
+ -- dropped in a later contract, once nothing has read it for a while.
4
+ update orgs o
5
+ set app_limit = u.app_limit
6
+ from users u
7
+ where lower(u.username) = lower(o.name) and o.kind = 'personal'
8
+ and o.app_limit is null and u.app_limit is not null;
@@ -0,0 +1,10 @@
1
+ -- An app must be in an organisation.
2
+ --
3
+ -- Until now `org_id` could be null, which was right while nothing read it — the
4
+ -- backfill had to be allowed to leave a gap rather than fail. Now that every
5
+ -- tenancy question is `where org_id = $1`, a null is an app that exists and
6
+ -- that nobody, including its owner, can see. Better refused at write time.
7
+ --
8
+ -- `heychunky register verify` counts apps in no organisation, and it must read
9
+ -- zero before this can apply.
10
+ alter table apps alter column org_id set not null;
@@ -0,0 +1,11 @@
1
+ -- Names become unique per organisation rather than across the factory.
2
+ --
3
+ -- `apps_org_name_key` has been carrying this since 0002; this only removes the
4
+ -- global rule it made redundant. Keeping both would mean the first organisation
5
+ -- to call an app `blog` took the name from everybody, which is the whole thing
6
+ -- an organisation is supposed to stop.
7
+ --
8
+ -- Provider names are a separate question and stay global: a repository and a
9
+ -- Vercel project still cannot collide, and provisioning is what has to answer
10
+ -- for that.
11
+ drop index apps_name_key;
@@ -0,0 +1,4 @@
1
+ -- Absorbed by `org_members`. Never held a row, and has had no reader since
2
+ -- `orgs.ts` replaced `contexts.ts`, nor a writer since `claimUsername` started
3
+ -- creating the personal organisation instead.
4
+ drop table context_members;
@@ -0,0 +1,2 @@
1
+ -- Absorbed by `orgs`. Dropped after `context_members`, which referenced it.
2
+ drop table contexts;