@microteia/console 2.41.0 → 2.43.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.
@@ -22,6 +22,28 @@
22
22
  - fix(command): what was broken, and what it did to whoever hit it.
23
23
  -->
24
24
 
25
+ #### [2.43.0] — 2026-11-10
26
+
27
+ - feat(plugin-init): with a terminal, asks the publisher and the prefix right after the namespace, each
28
+ with a short explanation, and records them in the seed `package.json` under `microteia.publisher` and
29
+ `microteia.prefix` — the keys `microteia-plugin create` reads. The publisher suggests the office name
30
+ (`project.name` in `microteia.yaml`); the prefix suggests the one `create` would (the namespace's last
31
+ segment when it fits, `com.acme` → `acme`); Enter accepts either. Until now both were asked by the first
32
+ `create`, so a value that never changes was chosen by whoever ran the first extension — often an agent.
33
+ - feat(plugin-init): `--publisher <name>` and `--prefix <acme>` answer without a terminal; an invalid
34
+ prefix is refused before anything is written. Without a terminal and without the flags, both are left
35
+ out and `create` asks for them once, as before. `--force` reuses the three values already recorded.
36
+
37
+ #### [2.42.0] — 2026-11-08
38
+
39
+ - feat(kernel/manifest)!: the optional prerequisite `self-service-tenants` and the backend parameter
40
+ `SELF_SERVICE_TENANTS` are gone. Whether a new account gets an organization of its own is now decided
41
+ by SYSTEM in Access Management › Users (stored in `system/registration`), and takes effect without a
42
+ deploy. A `SELF_SERVICE_TENANTS` line left in `.env.<projectId>` is ignored: an installation that had it
43
+ set to `false` must tick "Block automatic organizations" there once, after upgrading the kernel to
44
+ 9.214.0.
45
+ - docs(installation): `MICROTEIA.md` no longer cites `SELF_SERVICE_TENANTS` as a platform setting.
46
+
25
47
  #### [2.41.0] — 2026-11-07
26
48
 
27
49
  - feat(cli/publish): after the catalog is written, the source repository (the one `gh repo view` names
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Microteia Console 2.41.0
1
+ # Microteia Console 2.43.0
2
2
 
3
3
  Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
4
4
  what it needs are command-line tools that are already on the machine, listed at the bottom.
@@ -6,7 +6,7 @@ import { blank, bullets, heading, info, success, warn } from '../shared/ui.js';
6
6
  import { run } from '../shared/run.js';
7
7
  import { ENVIRONMENT_FILE, readEnvironment } from './environment.js';
8
8
  import { WORKSPACE_DIR } from './workspace.js';
9
- import { SDK_TARBALL, isValidNamespace, workspacePackageJson } from '../vendor/pluginWorkspaceTemplates.js';
9
+ import { SDK_TARBALL, isValidNamespace, isValidPrefix, suggestPrefix, workspacePackageJson, } from '../vendor/pluginWorkspaceTemplates.js';
10
10
  /**
11
11
  * `microteia plugin-init` — prepara `extensions/`, onde o escritório escreve extensão em TypeScript.
12
12
  *
@@ -108,40 +108,100 @@ export const refreshAuthoringWorkspace = async (root, officeName) => {
108
108
  return false;
109
109
  }
110
110
  };
111
- /** O que um `--force` anterior deixou gravado: reescrever a semente não é motivo para perguntar de novo. */
112
- const existingNamespace = async (directory) => {
111
+ /**
112
+ * O que um `--force` anterior deixou gravado: reescrever a semente não é motivo para perguntar de novo —
113
+ * e o prefixo, uma vez usado, não pode mudar.
114
+ */
115
+ const existingIdentity = async (directory) => {
113
116
  try {
114
117
  const pkg = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
115
- const value = pkg.microteia?.namespace;
116
- return typeof value === 'string' && isValidNamespace(value) ? value : null;
118
+ const { namespace, publisher, prefix } = pkg.microteia ?? {};
119
+ return {
120
+ ...(typeof namespace === 'string' && isValidNamespace(namespace) ? { namespace } : {}),
121
+ ...(typeof publisher === 'string' && publisher.trim() !== '' ? { publisher: publisher.trim() } : {}),
122
+ ...(typeof prefix === 'string' && isValidPrefix(prefix) ? { prefix } : {}),
123
+ };
117
124
  }
118
125
  catch {
119
- return null;
126
+ return {};
120
127
  }
121
128
  };
129
+ const invalidNamespace = (namespace) => new Error(`"${namespace}" is not a valid namespace. Use reverse-DNS, lowercase, from a domain your organization`
130
+ + ' controls — for example: --namespace com.acme');
131
+ const invalidPrefix = (prefix) => new Error(`"${prefix}" is not a valid prefix. Use 2 to 8 lowercase letters or digits, starting with a letter`
132
+ + ' — for example: --prefix acme');
133
+ /** Com sugestão, Enter a aceita — é o que torna a sugestão uma resposta, e não só um exemplo. */
134
+ const asker = (rl) => async (label, suggestion) => {
135
+ const answer = (await rl.question(suggestion === undefined ? `${label}: ` : `${label} [${suggestion}]: `)).trim();
136
+ return answer === '' && suggestion !== undefined ? suggestion : answer;
137
+ };
122
138
  /**
123
- * O namespace é OBRIGATÓRIO, e não tem padrão: qualquer padrão seria um prefixo que não é do escritório,
124
- * e o id que nasce dele é permanente. Sem terminal, só a flag responde.
139
+ * Namespace, publisher e prefix — os três valem para TODA extensão do diretório, e por isso são
140
+ * perguntados aqui, uma vez, e não no primeiro `create`: quem chegava a ele de um agente via o prefixo
141
+ * ser escolhido por quem não podia escolhê-lo.
142
+ *
143
+ * O namespace é OBRIGATÓRIO e não tem padrão: qualquer padrão seria um prefixo que não é do escritório,
144
+ * e o id que nasce dele é permanente. Sem terminal, só a flag o responde. Publisher e prefix, sem
145
+ * terminal e sem flag, ficam para o `create` — que já sabe perguntá-los.
125
146
  */
126
- const resolveNamespace = async (args, directory) => {
127
- const given = flagValue(args, 'namespace') ?? await existingNamespace(directory) ?? await askNamespace();
128
- const namespace = given.trim();
129
- if (!isValidNamespace(namespace)) {
130
- throw new Error(`"${namespace}" is not a valid namespace. Use reverse-DNS, lowercase, from a domain your organization`
131
- + ' controls — for example: --namespace com.acme');
132
- }
133
- return namespace;
134
- };
135
- const askNamespace = async () => {
147
+ const resolveIdentity = async (args, directory, officeName) => {
148
+ const stored = await existingIdentity(directory);
149
+ const given = {
150
+ namespace: flagValue(args, 'namespace')?.trim() ?? stored.namespace,
151
+ publisher: flagValue(args, 'publisher')?.trim() ?? stored.publisher,
152
+ prefix: flagValue(args, 'prefix')?.trim() ?? stored.prefix,
153
+ };
154
+ if (given.namespace !== undefined && !isValidNamespace(given.namespace))
155
+ throw invalidNamespace(given.namespace);
156
+ if (given.prefix !== undefined && !isValidPrefix(given.prefix))
157
+ throw invalidPrefix(given.prefix);
158
+ if (given.publisher === '')
159
+ throw new Error('The publisher cannot be empty — for example: --publisher "Acme Legal"');
136
160
  if (!process.stdin.isTTY) {
137
- throw new Error('A namespace is required: every extension written in this directory is named below it, and its ids\n'
138
- + 'are permanent. Pass it with --namespace, for example: microteia plugin-init --namespace com.acme');
161
+ if (given.namespace === undefined) {
162
+ throw new Error('A namespace is required: every extension written in this directory is named below it, and its ids\n'
163
+ + 'are permanent. Pass it with --namespace, for example: microteia plugin-init --namespace com.acme');
164
+ }
165
+ return { namespace: given.namespace, publisher: given.publisher, prefix: given.prefix };
166
+ }
167
+ if (given.namespace !== undefined && given.publisher !== undefined && given.prefix !== undefined) {
168
+ return { namespace: given.namespace, publisher: given.publisher, prefix: given.prefix };
139
169
  }
140
- info('Every extension written here is named below a namespace — the reverse-DNS of a domain your');
141
- info('organization controls, like com.acme. Its ids are permanent, so choose one that is yours.');
142
170
  const rl = createInterface({ input: process.stdin, output: process.stdout });
171
+ const ask = asker(rl);
143
172
  try {
144
- return (await rl.question('namespace: ')).trim();
173
+ let namespace = given.namespace;
174
+ if (namespace === undefined) {
175
+ blank();
176
+ info('NAMESPACE — every extension written here is named below it: "billing" becomes com.acme.billing.');
177
+ info('Use the reverse-DNS of a domain your organization controls. The ids are permanent, so choose one');
178
+ info('that is yours.');
179
+ namespace = await ask('namespace');
180
+ if (!isValidNamespace(namespace))
181
+ throw invalidNamespace(namespace);
182
+ }
183
+ let publisher = given.publisher;
184
+ if (publisher === undefined) {
185
+ blank();
186
+ info('PUBLISHER — who answers for these extensions. It is shown as the author of each one on the');
187
+ info('Extensions screen of every office that installs it. A name, not an id.');
188
+ publisher = await ask('publisher', officeName);
189
+ if (publisher === '')
190
+ throw new Error('The publisher cannot be empty.');
191
+ }
192
+ let prefix = given.prefix;
193
+ if (prefix === undefined) {
194
+ blank();
195
+ info('PREFIX — starts every name your extensions create: acme_clients for a collection, acme-notify for');
196
+ info('an automation. It keeps two publishers who both pick "clients" from colliding in the same office.');
197
+ info('2 to 8 lowercase letters or digits. It never changes: a new prefix would rename the collections');
198
+ info('and leave their records behind. The suggestion comes from the namespace.');
199
+ prefix = await ask('prefix', suggestPrefix(namespace));
200
+ if (!isValidPrefix(prefix))
201
+ throw invalidPrefix(prefix);
202
+ }
203
+ blank();
204
+ return { namespace, publisher, prefix };
145
205
  }
146
206
  finally {
147
207
  rl.close();
@@ -160,19 +220,25 @@ export const pluginInit = async (args) => {
160
220
  }
161
221
  const sdk = await locateSdk(root);
162
222
  const installDir = join(root, WORKSPACE_DIR, 'install');
163
- const namespace = await resolveNamespace(args, directory);
164
223
  heading(`Extension authoring for "${environment.project.name}"`);
224
+ const identity = await resolveIdentity(args, directory, environment.project.name);
225
+ const { namespace } = identity;
165
226
  await mkdir(directory, { recursive: true });
166
227
  // O caminho do tarball é relativo, para que o diretório continue válido se a instalação inteira for
167
228
  // movida — o que acontece assim que alguém a versiona num repositório do escritório.
168
- await writeFile(join(directory, 'package.json'), workspacePackageJson(environment.environment.id, relative(directory, sdk.path), namespace));
169
- success(`wrote the seed package.json (namespace ${namespace})`);
229
+ await writeFile(join(directory, 'package.json'), workspacePackageJson(environment.environment.id, relative(directory, sdk.path), identity));
230
+ success(`wrote the seed package.json (namespace ${namespace}`
231
+ + `${identity.publisher === undefined ? '' : `, publisher "${identity.publisher}"`}`
232
+ + `${identity.prefix === undefined ? '' : `, prefix ${identity.prefix}`})`);
233
+ if (identity.publisher === undefined || identity.prefix === undefined) {
234
+ info(' publisher and prefix not given — "npm run create" asks for them once, and records them there.');
235
+ }
170
236
  blank();
171
237
  info('Installing @microteia/sdk and writing the guides...');
172
238
  await syncAuthoringWorkspace(directory, sdk.path, installDir, environment.project.name);
173
239
  blank();
174
240
  info(`Extension authoring ready in ${relative(root, directory)}/ (kernel contract ${sdk.kernelVersion}).`);
175
- info(`Every extension created there is ${namespace}.<name> — recorded in package.json, under microteia.namespace.`);
241
+ info(`Every extension created there is ${namespace}.<name> — recorded in package.json, under microteia.`);
176
242
  bullets([
177
243
  `cd ${relative(process.cwd(), directory) || '.'}`,
178
244
  'npm run create',
package/lib/index.js CHANGED
@@ -67,7 +67,7 @@ const COMMANDS = [
67
67
  },
68
68
  {
69
69
  name: 'plugin-init',
70
- usage: 'microteia plugin-init --namespace <com.acme> [--dir <path>] [--force]',
70
+ usage: 'microteia plugin-init --namespace <com.acme> [--publisher <name>] [--prefix <acme>] [--dir <path>] [--force]',
71
71
  summary: 'Prepare "extensions/", to author extensions in TypeScript in your editor.',
72
72
  audience: 'office',
73
73
  execute: pluginInit,
@@ -141,20 +141,6 @@ export const MANUAL_PREREQUISITES = [
141
141
  + 'changes for anyone.',
142
142
  optional: true,
143
143
  },
144
- {
145
- id: 'self-service-tenants',
146
- title: 'Stop sign-up from opening an organization, if this installation is shared',
147
- detail: 'By default every new account gets an organization of its own, with that person as the owner. In an '
148
- + 'installation shared by several organizations — each one created on purpose — that lets anyone who '
149
- + 'can create an account open one. Write SELF_SERVICE_TENANTS=false in .env.<projectId> and run '
150
- + 'microteia deploy --only functions: from then on a new account waits to be added to an organization, and only the '
151
- + 'installation administrator (SYSTEM) opens organizations, in Access management > Organizations, '
152
- + 'naming an owner who already has an account. The first account of the installation gets one '
153
- + 'anyway. Any value other than empty, true or false is read as false. Two-step verification is '
154
- + 'required per organization and does not reach the installation administrator: protect SYSTEM '
155
- + 'accounts with a sign-in provider that enforces its own second factor.',
156
- optional: true,
157
- },
158
144
  ];
159
145
  /**
160
146
  * Os nomes das Cloud Functions saem de `functions/src/index.ts`, a mesma fonte e o mesmo parse que
@@ -87,4 +87,4 @@ export declare const consoleRoot: string;
87
87
  * de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
88
88
  * política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
89
89
  */
90
- export declare const CLI_VERSION = "2.41.0";
90
+ export declare const CLI_VERSION = "2.43.0";
@@ -94,4 +94,4 @@ export const consoleRoot = resolve(here, '..', '..');
94
94
  * de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
95
95
  * política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
96
96
  */
97
- export const CLI_VERSION = '2.41.0';
97
+ export const CLI_VERSION = '2.43.0';
@@ -197,7 +197,7 @@ setting:
197
197
 
198
198
  - **A setting of an extension, or of your own \`functions/\`** — run \`microteia provision\`. It notices the
199
199
  change (by a salted digest in \`microteia.yaml\`) and requeues exactly the functions that carry it.
200
- - **A setting of the platform itself** (\`SELF_SERVICE_TENANTS\`, \`AUDIT_ARCHIVE_BUCKET\`, …) — run
200
+ - **A setting of the platform itself** (\`AUDIT_ARCHIVE_BUCKET\`, …) — run
201
201
  \`microteia deploy --only functions\`. \`provision\` does not track them; the deploy compares each
202
202
  function's stamp, which includes its environment, and redeploys the ones whose value changed.
203
203
 
@@ -16,5 +16,22 @@ export declare const SDK_TARBALL = "microteia-sdk.tgz";
16
16
  * duplicá-los aqui era exatamente o problema. O `sync` os acrescenta logo depois do `npm install`, e o
17
17
  * segundo `npm install` os instala.
18
18
  */
19
- export declare const workspacePackageJson: (officeName: string, sdkPath: string, namespace: string) => string;
19
+ /**
20
+ * O que só quem roda o `plugin-init` pode decidir, e que vale para TODA extensão do diretório. O
21
+ * `publisher` e o `prefix` são opcionais: sem terminal e sem flag, quem os pergunta é o `create` do SDK,
22
+ * uma vez — o mesmo que faz num diretório anterior a eles.
23
+ */
24
+ export interface WorkspaceIdentity {
25
+ namespace: string;
26
+ publisher?: string;
27
+ prefix?: string;
28
+ }
29
+ export declare const workspacePackageJson: (officeName: string, sdkPath: string, identity: WorkspaceIdentity) => string;
20
30
  export declare const isValidNamespace: (value: string) => boolean;
31
+ export declare const isValidPrefix: (value: string) => boolean;
32
+ /**
33
+ * A sugestão do `create` do SDK (`suggestedPrefix`), repetida aqui para que o `plugin-init` ofereça o
34
+ * MESMO prefixo que o `create` ofereceria: o último segmento do namespace, se ele já serve
35
+ * (`com.acme` → `acme`), senão os primeiros caracteres que servem, senão quatro letras sorteadas.
36
+ */
37
+ export declare const suggestPrefix: (namespace: string, random?: () => number) => string;
@@ -11,12 +11,7 @@
11
11
  * escritório. É a semente — mínima de propósito, porque quem a completa é o `sync`.
12
12
  */
13
13
  export const SDK_TARBALL = 'microteia-sdk.tgz';
14
- /**
15
- * A semente do `package.json`. Não traz scripts nem devDependencies: os nomes de comando são do SDK, e
16
- * duplicá-los aqui era exatamente o problema. O `sync` os acrescenta logo depois do `npm install`, e o
17
- * segundo `npm install` os instala.
18
- */
19
- export const workspacePackageJson = (officeName, sdkPath, namespace) => JSON.stringify({
14
+ export const workspacePackageJson = (officeName, sdkPath, identity) => JSON.stringify({
20
15
  name: `${officeName}-extensions`,
21
16
  version: '0.0.0',
22
17
  private: true,
@@ -26,9 +21,14 @@ export const workspacePackageJson = (officeName, sdkPath, namespace) => JSON.str
26
21
  // traz o SDK novo é `npm run sdk:refresh` — ou o `microteia unpack`, que o faz sozinho.
27
22
  '@microteia/sdk': `file:${sdkPath}`,
28
23
  },
29
- // O prefixo de todo id escrito neste diretório. É o SDK que o lê (`microteia-plugin create`, o MCP);
30
- // o Console só o recebe de quem roda o `plugin-init`, que é a única pessoa que pode decidi-lo.
31
- microteia: { namespace },
24
+ // É o SDK que lê estas chaves (`microteia-plugin create`, o MCP); o Console só as recebe de quem roda
25
+ // o `plugin-init`, que é a única pessoa que pode decidi-las. Chave sem valor fica de fora, e não
26
+ // vazia: o `create` só pergunta o que não acha.
27
+ microteia: {
28
+ namespace: identity.namespace,
29
+ ...(identity.publisher === undefined ? {} : { publisher: identity.publisher }),
30
+ ...(identity.prefix === undefined ? {} : { prefix: identity.prefix }),
31
+ },
32
32
  }, null, 2) + '\n';
33
33
  /**
34
34
  * O formato de id de extensão (`isValidPluginId`, no domínio) — repetido porque o Console não importa
@@ -37,3 +37,24 @@ export const workspacePackageJson = (officeName, sdkPath, namespace) => JSON.str
37
37
  */
38
38
  const NAMESPACE_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9-]*)+$/;
39
39
  export const isValidNamespace = (value) => NAMESPACE_PATTERN.test(value);
40
+ /**
41
+ * O formato do prefixo de artefato (`ARTIFACT_PREFIX_PATTERN`, no domínio) — repetido pela mesma razão do
42
+ * namespace, e preso a ele pelo mesmo teste.
43
+ */
44
+ const PREFIX_PATTERN = /^[a-z][a-z0-9]{1,7}$/;
45
+ const PREFIX_MAX_LENGTH = 8;
46
+ const GENERATED_PREFIX_LENGTH = 4;
47
+ const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
48
+ export const isValidPrefix = (value) => PREFIX_PATTERN.test(value);
49
+ /**
50
+ * A sugestão do `create` do SDK (`suggestedPrefix`), repetida aqui para que o `plugin-init` ofereça o
51
+ * MESMO prefixo que o `create` ofereceria: o último segmento do namespace, se ele já serve
52
+ * (`com.acme` → `acme`), senão os primeiros caracteres que servem, senão quatro letras sorteadas.
53
+ */
54
+ export const suggestPrefix = (namespace, random = Math.random) => {
55
+ const last = (namespace.split('.').pop() ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
56
+ const candidate = last.replace(/^[0-9]+/, '').slice(0, PREFIX_MAX_LENGTH);
57
+ if (isValidPrefix(candidate))
58
+ return candidate;
59
+ return Array.from({ length: GENERATED_PREFIX_LENGTH }, () => LETTERS[Math.floor(random() * LETTERS.length) % LETTERS.length]).join('');
60
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microteia/console",
3
- "version": "2.41.0",
3
+ "version": "2.43.0",
4
4
  "description": "Install and update Microteia in a Firebase project you own.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "repository": {