@factorialco/fcode-cli 3.5.1 → 3.6.1

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 CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @factorialco/fcode-cli
20
20
  $ fcode COMMAND
21
21
  running command...
22
22
  $ fcode (--version)
23
- @factorialco/fcode-cli/3.5.1 linux-x64 node-v22.22.3
23
+ @factorialco/fcode-cli/3.6.1 linux-x64 node-v22.23.4
24
24
  $ fcode --help [COMMAND]
25
25
  USAGE
26
26
  $ fcode COMMAND
@@ -14,7 +14,8 @@ export default class Http extends Command {
14
14
  * Enforces the webhook's configured auth, simulating the cloud webhook endpoint: the effective
15
15
  * configuration names a variable, whose value is looked up in the workspace variables and compared
16
16
  * against the configured header. A missing or malformed header, an undefined variable, a mismatch,
17
- * or a webhook inheriting a workspace configuration that settings.json does not have, is rejected.
17
+ * or a webhook inheriting a workspace configuration that is cleared or neither set nor inherited, is
18
+ * rejected.
18
19
  * Returns true when the request may proceed, false after writing a 403.
19
20
  */
20
21
  private _checkWebhookAuth;
@@ -122,7 +122,8 @@ class Http extends core_1.Command {
122
122
  * Enforces the webhook's configured auth, simulating the cloud webhook endpoint: the effective
123
123
  * configuration names a variable, whose value is looked up in the workspace variables and compared
124
124
  * against the configured header. A missing or malformed header, an undefined variable, a mismatch,
125
- * or a webhook inheriting a workspace configuration that settings.json does not have, is rejected.
125
+ * or a webhook inheriting a workspace configuration that is cleared or neither set nor inherited, is
126
+ * rejected.
126
127
  * Returns true when the request may proceed, false after writing a 403.
127
128
  */
128
129
  async _checkWebhookAuth(req, res, processRepository, settingsRepository, slug) {
@@ -137,9 +138,10 @@ class Http extends core_1.Command {
137
138
  // Read per request, like the process metadata above, so editing settings.json while the server
138
139
  // runs takes effect without a restart.
139
140
  const teamWebhookAuth = (await settingsRepository.find()).webhookAuth;
141
+ const inheritedTeamWebhookAuth = settingsRepository.findInherited().webhookAuth;
140
142
  let effectiveAuth;
141
143
  try {
142
- effectiveAuth = (0, webhook_auth_1.resolveEffectiveAuth)(webhook, teamWebhookAuth);
144
+ effectiveAuth = (0, webhook_auth_1.resolveEffectiveAuth)(webhook, teamWebhookAuth, inheritedTeamWebhookAuth);
143
145
  }
144
146
  catch (error) {
145
147
  // Fails closed, and says why: otherwise this reads as a wrong token rather than a local
@@ -5,6 +5,7 @@ const processes_command_1 = require("../../core/commands/processes-command");
5
5
  const types_1 = require("../../core/types");
6
6
  const cli_1 = require("../../utils/cli");
7
7
  const settings_1 = require("../../config/settings");
8
+ const retired_app_role_1 = require("../../services/retired-app-role");
8
9
  class ProcessesPush extends processes_command_1.default {
9
10
  async pushVersioned() {
10
11
  const processes = (await this._resourceRepository.findAll()).filter((process) => process.id && !process.deletion);
@@ -25,6 +26,10 @@ class ProcessesPush extends processes_command_1.default {
25
26
  await Promise.allSettled(allPromises);
26
27
  }
27
28
  async run() {
29
+ const appRoleError = (0, retired_app_role_1.retiredAppRoleError)(await this.getLocalResources());
30
+ if (appRoleError) {
31
+ this.error(appRoleError);
32
+ }
28
33
  await super.pushResources();
29
34
  const ignoreProcessVersions = (await this.globalConfig.getSetting(settings_1.SettingKeys.IGNORE_PROCESS_VERSIONS)) === 'true';
30
35
  if (!ignoreProcessVersions) {
@@ -5,6 +5,7 @@ const api_client_command_1 = require("../../core/commands/api-client-command");
5
5
  const settings_1 = require("../../repositories/settings");
6
6
  const types_1 = require("../../core/types");
7
7
  const cli_1 = require("../../utils/cli");
8
+ const gitignore_1 = require("../../utils/gitignore");
8
9
  class SettingsPullCommand extends api_client_command_1.default {
9
10
  constructor() {
10
11
  super(...arguments);
@@ -32,6 +33,10 @@ class SettingsPullCommand extends api_client_command_1.default {
32
33
  const settings = await this._settingsRepository.fromRaw(raw);
33
34
  this._settingsRepository.applyCloudVersioning(settings, versions, aliases);
34
35
  this._settingsRepository.save(settings);
36
+ this._settingsRepository.saveInherited(await this._settingsRepository.inheritedFromRaw(raw));
37
+ // A read-only copy of the parents' values: keep it out of git even when this command runs on
38
+ // its own rather than inside `fcode pull`.
39
+ await (0, gitignore_1.syncInheritedGitignore)();
35
40
  await this.workspaceConfig.setSettingsStoredHash(settings.contentHash);
36
41
  }
37
42
  }
@@ -8,6 +8,12 @@ export default class SettingsPushCommand extends ApiClientCommand {
8
8
  };
9
9
  _settingsRepository: SettingsRepository;
10
10
  run(): Promise<void>;
11
+ /**
12
+ * A settings.json some CLI synced (there is a stored hash) without marking it inheritance-aware was
13
+ * written by a CLI older than settings inheritance, with every effective value in it. Only a
14
+ * workspace with parents is affected: without any, its effective settings are its own.
15
+ */
16
+ private pulledByAPreInheritanceCli;
11
17
  /** Makes the cloud's versions/aliases match settings.json; returns failed manifest entries. */
12
18
  private syncVersioning;
13
19
  }
@@ -2,10 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const color_1 = require("@oclif/color");
4
4
  const api_client_command_1 = require("../../core/commands/api-client-command");
5
+ const errors_1 = require("../../core/errors");
5
6
  const settings_1 = require("../../repositories/settings");
6
7
  const types_1 = require("../../core/types");
7
8
  const team_versioning_1 = require("../../services/team-versioning");
8
9
  const cli_1 = require("../../utils/cli");
10
+ const gitignore_1 = require("../../utils/gitignore");
9
11
  class SettingsPushCommand extends api_client_command_1.default {
10
12
  constructor() {
11
13
  super(...arguments);
@@ -23,30 +25,19 @@ class SettingsPushCommand extends api_client_command_1.default {
23
25
  cli_1.default.log('Workspace settings already up to date');
24
26
  return;
25
27
  }
26
- const update = { parentTeams: settings.parentTeams };
27
- if (settings.zoneId !== undefined) {
28
- update.zoneId = settings.zoneId;
28
+ if (await this.pulledByAPreInheritanceCli(settings, storedHash)) {
29
+ throw new errors_1.PreInheritanceSettingsFile();
29
30
  }
30
- if (settings.errorHandlerConfig) {
31
- const processId = await this._settingsRepository.resolveSlugToId(settings.errorHandlerConfig.processSlug);
32
- update.executionErrorHandlerConfig = { processId, tag: settings.errorHandlerConfig.tag ?? null };
33
- }
34
- // Sent unconditionally, null arm included: settings.json is the source of truth, so removing
35
- // webhookAuth from it must clear the cloud configuration rather than leave it dangling.
36
- update.webhookAuth = settings.webhookAuth
37
- ? { headerName: settings.webhookAuth.headerName ?? null, variableKey: settings.webhookAuth.variableKey }
38
- : { variableKey: null };
39
- // Sent unconditionally for the same reason as webhookAuth: settings.json is the source of truth,
40
- // so removing primaryLocale from it must clear the cloud setting. Empty string is the clear.
41
- update.primaryLocale = settings.primaryLocale ?? '';
42
- await this._fcodeGraphQLClient.pushTeam(update);
31
+ const pushed = await this._fcodeGraphQLClient.pushTeam(await this._settingsRepository.toTeamUpdate(settings));
32
+ // What the workspace now inherits follows from what was just pushed.
33
+ this._settingsRepository.saveInherited(await this._settingsRepository.inheritedFromRaw(pushed));
34
+ // A read-only copy of the parents' values: keep it out of git even when this command runs on
35
+ // its own rather than inside `fcode pull`.
36
+ await (0, gitignore_1.syncInheritedGitignore)();
43
37
  const failedEntities = await this.syncVersioning(settings, force);
44
38
  // Re-read the cloud state so server-assigned fields (createdAt) and declined deletions
45
39
  // land back in settings.json, then hash exactly what was written.
46
- const [versions, aliases] = await Promise.all([
47
- this._fcodeGraphQLClient.allTeamVersions(),
48
- this._fcodeGraphQLClient.allTeamVersionAliases(),
49
- ]);
40
+ const [versions, aliases] = await Promise.all([this._fcodeGraphQLClient.allTeamVersions(), this._fcodeGraphQLClient.allTeamVersionAliases()]);
50
41
  this._settingsRepository.applyCloudVersioning(settings, versions, aliases);
51
42
  this._settingsRepository.save(settings);
52
43
  await this.workspaceConfig.setSettingsStoredHash(settings.contentHash);
@@ -57,12 +48,17 @@ class SettingsPushCommand extends api_client_command_1.default {
57
48
  this.error(`workspace versions pushed with ${failedEntities} failed entities`, { exit: 1 });
58
49
  }
59
50
  }
51
+ /**
52
+ * A settings.json some CLI synced (there is a stored hash) without marking it inheritance-aware was
53
+ * written by a CLI older than settings inheritance, with every effective value in it. Only a
54
+ * workspace with parents is affected: without any, its effective settings are its own.
55
+ */
56
+ async pulledByAPreInheritanceCli(settings, storedHash) {
57
+ return storedHash !== undefined && settings.parentTeams.length > 0 && !(await this.workspaceConfig.isSettingsInheritanceAware());
58
+ }
60
59
  /** Makes the cloud's versions/aliases match settings.json; returns failed manifest entries. */
61
60
  async syncVersioning(settings, force) {
62
- const [cloudVersions, cloudAliases] = await Promise.all([
63
- this._fcodeGraphQLClient.allTeamVersions(),
64
- this._fcodeGraphQLClient.allTeamVersionAliases(),
65
- ]);
61
+ const [cloudVersions, cloudAliases] = await Promise.all([this._fcodeGraphQLClient.allTeamVersions(), this._fcodeGraphQLClient.allTeamVersionAliases()]);
66
62
  const plan = (0, team_versioning_1.planTeamVersioningSync)(settings, cloudVersions, cloudAliases);
67
63
  if ((0, team_versioning_1.teamVersioningSyncPlanIsEmpty)(plan)) {
68
64
  return 0;
@@ -96,8 +92,7 @@ class SettingsPushCommand extends api_client_command_1.default {
96
92
  return failedEntities;
97
93
  }
98
94
  }
99
- SettingsPushCommand.description = 'Update cloud workspace settings (timezone, error handler, inheritance, webhook auth) and sync ' +
100
- 'workspace versions/aliases from settings.json';
95
+ SettingsPushCommand.description = 'Update cloud workspace settings (timezone, error handler, inheritance, webhook auth) and sync ' + 'workspace versions/aliases from settings.json';
101
96
  SettingsPushCommand.examples = ['$ fcode settings:push'];
102
97
  SettingsPushCommand.flags = types_1.ForceFlag;
103
98
  exports.default = SettingsPushCommand;
@@ -21,6 +21,12 @@ declare class WorkspaceConfig extends Config {
21
21
  getStoredHash(resource: Resource): Promise<string>;
22
22
  getSettingsStoredHash(): Promise<string | undefined>;
23
23
  setSettingsStoredHash(contentHash: string): Promise<void>;
24
+ /**
25
+ * Whether settings.json was last pulled or pushed by a CLI that knows settings inherit. One that
26
+ * did not wrote the values the workspace inherits as if they were its own, and records the hash
27
+ * without this mark.
28
+ */
29
+ isSettingsInheritanceAware(): Promise<boolean>;
24
30
  add(resourceType: string, resourceMetadata: ResourceMetadata): Promise<void>;
25
31
  remove(resourceType: string, slug: string): Promise<void>;
26
32
  }
@@ -139,7 +139,15 @@ class WorkspaceConfig extends index_1.default {
139
139
  }
140
140
  async setSettingsStoredHash(contentHash) {
141
141
  const team = await this.getActiveTeam();
142
- await this.setTeam({ ...team, team: { contentHash } });
142
+ await this.setTeam({ ...team, team: { contentHash, inheritanceAware: true } });
143
+ }
144
+ /**
145
+ * Whether settings.json was last pulled or pushed by a CLI that knows settings inherit. One that
146
+ * did not wrote the values the workspace inherits as if they were its own, and records the hash
147
+ * without this mark.
148
+ */
149
+ async isSettingsInheritanceAware() {
150
+ return (await this.getActiveTeam()).team?.inheritanceAware === true;
143
151
  }
144
152
  async add(resourceType, resourceMetadata) {
145
153
  const team = await this.getActiveTeam();
@@ -54,6 +54,13 @@ export declare class TeamAccessDenied extends CLIError {
54
54
  export declare class NotFcodeWorkspace extends CLIError {
55
55
  constructor();
56
56
  }
57
+ /**
58
+ * settings.json was last synced by a CLI older than settings inheritance, which wrote the settings
59
+ * the workspace inherits as if they were its own: pushing it would stop the workspace inheriting them.
60
+ */
61
+ export declare class PreInheritanceSettingsFile extends CLIError {
62
+ constructor();
63
+ }
57
64
  export declare class DependenciesNotInstalled extends CLIError {
58
65
  constructor(programmingLanguage: ProgrammingLanguage);
59
66
  }
@@ -63,3 +70,10 @@ export declare class ProcessDependenciesNotInstalled extends CLIError {
63
70
  export declare class ErrorExecutingProcess extends CLIError {
64
71
  constructor(message: string);
65
72
  }
73
+ /**
74
+ * The platform answered without the team's inheritable settings: it predates settings inheritance,
75
+ * and reading its settings would take every one of them for inherited.
76
+ */
77
+ export declare class PlatformPredatesSettingsInheritance extends CLIError {
78
+ constructor();
79
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ErrorExecutingProcess = exports.ProcessDependenciesNotInstalled = exports.DependenciesNotInstalled = exports.NotFcodeWorkspace = exports.TeamAccessDenied = exports.NotAuthenticated = exports.CloudProgrammingLanguageManifestNotFound = exports.LocalProgrammingLanguageManifestNotFound = exports.CloudLocaleNotFound = exports.LocalLocaleNotFound = exports.CloudVariableNotFound = exports.LocalVariableNotFound = exports.CloudScriptLibraryNotFound = exports.LocalScriptLibraryNotFound = exports.CloudProcessNotFound = exports.LocalProcessNotFound = exports.ResourceNotFound = exports.CLIError = void 0;
3
+ exports.PlatformPredatesSettingsInheritance = exports.ErrorExecutingProcess = exports.ProcessDependenciesNotInstalled = exports.DependenciesNotInstalled = exports.PreInheritanceSettingsFile = exports.NotFcodeWorkspace = exports.TeamAccessDenied = exports.NotAuthenticated = exports.CloudProgrammingLanguageManifestNotFound = exports.LocalProgrammingLanguageManifestNotFound = exports.CloudLocaleNotFound = exports.LocalLocaleNotFound = exports.CloudVariableNotFound = exports.LocalVariableNotFound = exports.CloudScriptLibraryNotFound = exports.LocalScriptLibraryNotFound = exports.CloudProcessNotFound = exports.LocalProcessNotFound = exports.ResourceNotFound = exports.CLIError = void 0;
4
4
  exports.teamAccessDeniedMessage = teamAccessDeniedMessage;
5
5
  const global_1 = require("../config/global");
6
6
  class CLIError extends Error {
@@ -145,6 +145,25 @@ class NotFcodeWorkspace extends CLIError {
145
145
  }
146
146
  }
147
147
  exports.NotFcodeWorkspace = NotFcodeWorkspace;
148
+ /**
149
+ * settings.json was last synced by a CLI older than settings inheritance, which wrote the settings
150
+ * the workspace inherits as if they were its own: pushing it would stop the workspace inheriting them.
151
+ */
152
+ class PreInheritanceSettingsFile extends CLIError {
153
+ constructor() {
154
+ super('settings.json was last synced by an fcode CLI that predates settings inheritance: it holds the settings this ' +
155
+ 'workspace inherits from its parent workspaces as if they were its own, and pushing it would stop the ' +
156
+ 'workspace inheriting them.', {
157
+ code: 'ERR_PRE_INHERITANCE_SETTINGS_FILE',
158
+ exit: 1,
159
+ suggestions: [
160
+ 'fcode settings:pull, then push again',
161
+ 'fcode settings:pull --force if settings.json has local changes, then reapply them and push again',
162
+ ],
163
+ });
164
+ }
165
+ }
166
+ exports.PreInheritanceSettingsFile = PreInheritanceSettingsFile;
148
167
  class DependenciesNotInstalled extends CLIError {
149
168
  constructor(programmingLanguage) {
150
169
  super(`Dependencies not installed.`, {
@@ -175,3 +194,18 @@ class ErrorExecutingProcess extends CLIError {
175
194
  }
176
195
  }
177
196
  exports.ErrorExecutingProcess = ErrorExecutingProcess;
197
+ /**
198
+ * The platform answered without the team's inheritable settings: it predates settings inheritance,
199
+ * and reading its settings would take every one of them for inherited.
200
+ */
201
+ class PlatformPredatesSettingsInheritance extends CLIError {
202
+ constructor() {
203
+ super('This Factorial Code platform predates settings inheritance, which this fcode CLI requires to read and ' +
204
+ 'push workspace settings.', {
205
+ code: 'ERR_PLATFORM_PREDATES_SETTINGS_INHERITANCE',
206
+ exit: 1,
207
+ suggestions: ['Wait for the platform to be updated, or use an fcode CLI older than 3.6.0 until then'],
208
+ });
209
+ }
210
+ }
211
+ exports.PlatformPredatesSettingsInheritance = PlatformPredatesSettingsInheritance;
@@ -49,6 +49,7 @@ export interface TeamMetadata {
49
49
  };
50
50
  team?: {
51
51
  contentHash?: string;
52
+ inheritanceAware?: boolean;
52
53
  };
53
54
  }
54
55
  export interface RemoteMetadata {
@@ -141,6 +142,10 @@ export type AppRole = 'INSTALL' | 'SETTINGS' | 'USER_FACING_FORM' | 'UNINSTALL'
141
142
  export type FormAuthMode = 'NONE' | 'FACTORIAL';
142
143
  export interface ProcessForm {
143
144
  enabled: boolean;
145
+ /**
146
+ * @deprecated Retired: push refuses it and pull no longer writes it. Kept so a metadata.json that
147
+ * still carries it can be read and reported.
148
+ */
144
149
  appRole?: AppRole;
145
150
  authMode?: FormAuthMode;
146
151
  }
@@ -397,24 +402,34 @@ export interface ParentTeamRef {
397
402
  slug: string;
398
403
  version?: string;
399
404
  }
400
- /** The workspace's settings.json: what `fcode settings:pull|push` syncs against the cloud team. */
405
+ /**
406
+ * The workspace's settings.json: what `fcode settings:pull|push` syncs against the cloud team. It
407
+ * holds the team's own values only: an absent setting inherits, and null (error handler and webhook
408
+ * auth only) clears it, a parent's value included.
409
+ */
401
410
  export declare class WorkspaceSettings extends Resource {
402
411
  id: string | undefined;
403
412
  slug: string;
404
413
  zoneId?: string;
405
- errorHandlerConfig?: ErrorHandlerConfig;
414
+ errorHandlerConfig?: ErrorHandlerConfig | null;
406
415
  parentTeams: ParentTeamRef[];
407
- webhookAuth?: WebhookAuth;
416
+ webhookAuth?: WebhookAuth | null;
408
417
  primaryLocale?: string;
409
418
  versions?: TeamVersionFileEntry[];
410
419
  aliases?: TeamAliasFileEntry[];
411
- constructor(slug: string, parentTeams: ParentTeamRef[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig, webhookAuth?: WebhookAuth, primaryLocale?: string);
420
+ constructor(slug: string, parentTeams: ParentTeamRef[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig | null, webhookAuth?: WebhookAuth | null, primaryLocale?: string);
412
421
  get contentHash(): string;
413
422
  }
414
423
  export interface RawErrorHandlerConfig {
415
424
  processId: string;
416
425
  tag?: string | null;
417
426
  }
427
+ export type InheritableSetting = 'ERROR_HANDLER' | 'PARAMS_SCHEMA_VALIDATION' | 'PRIMARY_LOCALE' | 'WEBHOOK_AUTH' | 'ZONE_ID';
428
+ export type SettingMode = 'INHERIT' | 'NONE' | 'OWN';
429
+ export interface SettingOrigin {
430
+ mode: SettingMode;
431
+ from?: string | null;
432
+ }
418
433
  export interface RawTeamSettings {
419
434
  slug: string;
420
435
  zoneId?: string;
@@ -422,16 +437,34 @@ export interface RawTeamSettings {
422
437
  parentTeams: ParentTeamRef[];
423
438
  webhookAuth?: WebhookAuth | null;
424
439
  primaryLocale?: string | null;
440
+ origins?: Record<InheritableSetting, SettingOrigin>;
425
441
  }
426
- export interface TeamUpdateInput {
442
+ export interface InheritedWorkspaceSettings {
427
443
  zoneId?: string;
428
- parentTeams?: ParentTeamRef[];
429
- executionErrorHandlerConfig?: RawErrorHandlerConfig | null;
430
- webhookAuth?: {
431
- headerName?: string | null;
432
- variableKey?: string | null;
433
- } | null;
444
+ errorHandlerConfig?: ErrorHandlerConfig;
445
+ webhookAuth?: WebhookAuth;
434
446
  primaryLocale?: string;
447
+ inheritedFrom: Record<string, string>;
448
+ }
449
+ export type SettingInput<T> = {
450
+ mode: 'INHERIT';
451
+ } | {
452
+ mode: 'NONE';
453
+ } | {
454
+ mode: 'OWN';
455
+ value: T;
456
+ };
457
+ export interface TeamUpdateInput {
458
+ parentTeams?: ParentTeamRef[];
459
+ inheritableSettings?: {
460
+ zoneId?: SettingInput<string>;
461
+ errorHandlerConfig?: SettingInput<RawErrorHandlerConfig>;
462
+ webhookAuth?: SettingInput<{
463
+ headerName?: string | null;
464
+ variableKey: string;
465
+ }>;
466
+ primaryLocale?: SettingInput<string>;
467
+ };
435
468
  }
436
469
  export type ResourceMetadata = {
437
470
  [key: string]: any;
@@ -261,7 +261,11 @@ class ProgrammingLanguageManifest extends Resource {
261
261
  }
262
262
  }
263
263
  exports.ProgrammingLanguageManifest = ProgrammingLanguageManifest;
264
- /** The workspace's settings.json: what `fcode settings:pull|push` syncs against the cloud team. */
264
+ /**
265
+ * The workspace's settings.json: what `fcode settings:pull|push` syncs against the cloud team. It
266
+ * holds the team's own values only: an absent setting inherits, and null (error handler and webhook
267
+ * auth only) clears it, a parent's value included.
268
+ */
265
269
  class WorkspaceSettings extends Resource {
266
270
  constructor(slug, parentTeams, zoneId, errorHandlerConfig, webhookAuth, primaryLocale) {
267
271
  super();
@@ -293,6 +297,14 @@ class WorkspaceSettings extends Resource {
293
297
  if (this.primaryLocale) {
294
298
  payload.primaryLocale = this.primaryLocale;
295
299
  }
300
+ // Cleared settings hash apart from inherited ones; inserted only when any is cleared.
301
+ const cleared = [
302
+ ...(this.errorHandlerConfig === null ? ['ERROR_HANDLER'] : []),
303
+ ...(this.webhookAuth === null ? ['WEBHOOK_AUTH'] : []),
304
+ ];
305
+ if (cleared.length > 0) {
306
+ payload.cleared = cleared;
307
+ }
296
308
  // Versions hash only their tags and aliases their name->tag pairs: createdAt is
297
309
  // server-assigned and a comment is immutable once published (there is no update mutation),
298
310
  // so neither can represent a pushable local change. Inserted only when non-empty, again to
@@ -1,6 +1,7 @@
1
- import { RawTeamSettings, TeamVersion, TeamVersionAlias, WorkspaceSettings } from '../core/types';
1
+ import { InheritedWorkspaceSettings, RawTeamSettings, TeamUpdateInput, TeamVersion, TeamVersionAlias, WorkspaceSettings } from '../core/types';
2
2
  import WorkspaceConfig from '../config/workspace';
3
3
  export declare const SETTINGS_FILENAME = "settings.json";
4
+ export declare const INHERITED_SETTINGS_FILENAME = "settings.inherited.json";
4
5
  /**
5
6
  * Workspaces cloned before the file was renamed carry team.json. Renaming it on first access keeps
6
7
  * the stored sync hash valid (it is computed over the content, not the name), so the workspace does
@@ -22,8 +23,23 @@ export default class SettingsRepository {
22
23
  exists(): boolean;
23
24
  find(): Promise<WorkspaceSettings>;
24
25
  save(settings: WorkspaceSettings): void;
25
- /** Builds the settings from the cloud's raw payload, resolving the error-handler id to a slug. */
26
+ /** The settings a local run inherits; empty when the workspace inherits none. */
27
+ findInherited(): InheritedWorkspaceSettings;
28
+ /** Writes settings.inherited.json, or removes it when the workspace inherits no setting. */
29
+ saveInherited(inherited: InheritedWorkspaceSettings | undefined): void;
30
+ /**
31
+ * Builds the settings.json view of the cloud's raw payload: only the team's own values, null for a
32
+ * cleared setting, and nothing for an inherited or default one. Resolves the error-handler id to a
33
+ * slug.
34
+ */
26
35
  fromRaw(raw: RawTeamSettings): Promise<WorkspaceSettings>;
36
+ /** The settings.inherited.json view of the cloud's raw payload; undefined when nothing is inherited. */
37
+ inheritedFromRaw(raw: RawTeamSettings): Promise<InheritedWorkspaceSettings | undefined>;
38
+ /**
39
+ * settings.json is the source of truth for the team's own values: a setting absent from it is
40
+ * reset to inherit, null leaves the team with none (a parent's included), and a value sets it.
41
+ */
42
+ toTeamUpdate(settings: WorkspaceSettings): Promise<TeamUpdateInput>;
27
43
  /**
28
44
  * Writes the cloud's workspace versions and aliases into the settings as the settings.json
29
45
  * versioning fields (sorted for stable git diffs). Cloud entries always carry a createdAt; only
@@ -32,6 +48,10 @@ export default class SettingsRepository {
32
48
  applyCloudVersioning(settings: WorkspaceSettings, versions: TeamVersion[], aliases: TeamVersionAlias[]): void;
33
49
  resolveSlugToId(processSlug: string): Promise<string>;
34
50
  resolveIdToSlug(processId: string): Promise<string>;
51
+ /** Where each setting comes from; an API without it predates settings inheritance. */
52
+ private originsOf;
53
+ private resolveErrorHandler;
54
+ private parseString;
35
55
  private parseErrorHandler;
36
56
  /**
37
57
  * settings.json is hand-edited to declare versions, so entries without a tag are dropped and a
@@ -1,13 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SETTINGS_FILENAME = void 0;
3
+ exports.INHERITED_SETTINGS_FILENAME = exports.SETTINGS_FILENAME = void 0;
4
4
  exports.migrateLegacySettingsFile = migrateLegacySettingsFile;
5
5
  const fs = require("fs-extra");
6
6
  const path = require("node:path");
7
7
  const types_1 = require("../core/types");
8
8
  const workspace_1 = require("../config/workspace");
9
+ const errors_1 = require("../core/errors");
9
10
  const file_utils_1 = require("../utils/file-utils");
10
11
  exports.SETTINGS_FILENAME = 'settings.json';
12
+ exports.INHERITED_SETTINGS_FILENAME = 'settings.inherited.json';
13
+ /** The inheritable settings settings.json manages, by their field name there. */
14
+ const SETTING_FIELDS = {
15
+ ZONE_ID: 'zoneId',
16
+ ERROR_HANDLER: 'errorHandlerConfig',
17
+ WEBHOOK_AUTH: 'webhookAuth',
18
+ PRIMARY_LOCALE: 'primaryLocale',
19
+ };
11
20
  const LEGACY_SETTINGS_FILENAME = 'team.json';
12
21
  /**
13
22
  * Workspaces cloned before the file was renamed carry team.json. Renaming it on first access keeps
@@ -22,6 +31,14 @@ function migrateLegacySettingsFile(workspaceDir = process.cwd()) {
22
31
  fs.renameSync(legacy, current);
23
32
  }
24
33
  }
34
+ /** A setting settings.json holds a value of or leaves out: its own value, or inherit. */
35
+ function ownOrInherit(value) {
36
+ return value === undefined ? { mode: 'INHERIT' } : { mode: 'OWN', value };
37
+ }
38
+ /** A setting settings.json leaves out (inherit) or sets to null (none). */
39
+ function noneOrInherit(value) {
40
+ return value === null ? { mode: 'NONE' } : { mode: 'INHERIT' };
41
+ }
25
42
  /**
26
43
  * Reads/writes the singleton settings.json (timezone, error handler, inheritance, webhook auth) at
27
44
  * the workspace root. Unlike collection resources this is not a ResourceRepositoryBase:
@@ -47,7 +64,7 @@ class SettingsRepository {
47
64
  raw = fs.readJsonSync(exports.SETTINGS_FILENAME) || {};
48
65
  }
49
66
  catch { }
50
- const settings = new types_1.WorkspaceSettings(slug, this.parseParentTeams(raw.parentTeams), raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig), this.parseWebhookAuth(raw.webhookAuth), typeof raw.primaryLocale === 'string' && raw.primaryLocale.length > 0 ? raw.primaryLocale : undefined);
67
+ const settings = new types_1.WorkspaceSettings(slug, this.parseParentTeams(raw.parentTeams), this.parseString(raw.zoneId), raw.errorHandlerConfig === null ? null : this.parseErrorHandler(raw.errorHandlerConfig), raw.webhookAuth === null ? null : this.parseWebhookAuth(raw.webhookAuth), this.parseString(raw.primaryLocale));
51
68
  if (Array.isArray(raw.versions)) {
52
69
  settings.versions = this.parseVersions(raw.versions);
53
70
  }
@@ -60,16 +77,97 @@ class SettingsRepository {
60
77
  migrateLegacySettingsFile();
61
78
  (0, file_utils_1.outputFileSyncIfChanged)(exports.SETTINGS_FILENAME, JSON.stringify(this.toFile(settings), null, 2));
62
79
  }
63
- /** Builds the settings from the cloud's raw payload, resolving the error-handler id to a slug. */
80
+ /** The settings a local run inherits; empty when the workspace inherits none. */
81
+ findInherited() {
82
+ let raw = {};
83
+ try {
84
+ raw = fs.readJsonSync(exports.INHERITED_SETTINGS_FILENAME) || {};
85
+ }
86
+ catch { }
87
+ return {
88
+ zoneId: this.parseString(raw.zoneId),
89
+ errorHandlerConfig: this.parseErrorHandler(raw.errorHandlerConfig),
90
+ webhookAuth: this.parseWebhookAuth(raw.webhookAuth),
91
+ primaryLocale: this.parseString(raw.primaryLocale),
92
+ inheritedFrom: raw.inheritedFrom ?? {},
93
+ };
94
+ }
95
+ /** Writes settings.inherited.json, or removes it when the workspace inherits no setting. */
96
+ saveInherited(inherited) {
97
+ if (!inherited) {
98
+ if (fs.pathExistsSync(exports.INHERITED_SETTINGS_FILENAME)) {
99
+ // Written read-only, so it has to be made writable before it can go.
100
+ fs.chmodSync(exports.INHERITED_SETTINGS_FILENAME, 0o644);
101
+ fs.removeSync(exports.INHERITED_SETTINGS_FILENAME);
102
+ }
103
+ return;
104
+ }
105
+ // Read-only like the other inherited files: the values belong to a parent workspace.
106
+ (0, file_utils_1.outputFileSyncIfChanged)(exports.INHERITED_SETTINGS_FILENAME, JSON.stringify(inherited, null, 2), { readOnly: true });
107
+ }
108
+ /**
109
+ * Builds the settings.json view of the cloud's raw payload: only the team's own values, null for a
110
+ * cleared setting, and nothing for an inherited or default one. Resolves the error-handler id to a
111
+ * slug.
112
+ */
64
113
  async fromRaw(raw) {
65
- let errorHandlerConfig;
66
- if (raw.errorHandlerConfig?.processId) {
67
- errorHandlerConfig = {
68
- processSlug: await this.resolveIdToSlug(raw.errorHandlerConfig.processId),
69
- tag: raw.errorHandlerConfig.tag ?? null,
70
- };
114
+ const origins = this.originsOf(raw);
115
+ const own = (setting, value) => {
116
+ const { mode } = origins[setting];
117
+ if (mode === 'NONE') {
118
+ return null;
119
+ }
120
+ return mode === 'OWN' ? value() : undefined;
121
+ };
122
+ return new types_1.WorkspaceSettings(raw.slug, raw.parentTeams ?? [], own('ZONE_ID', () => this.parseString(raw.zoneId)) ?? undefined, await own('ERROR_HANDLER', () => this.resolveErrorHandler(raw)), own('WEBHOOK_AUTH', () => this.parseWebhookAuth(raw.webhookAuth)), own('PRIMARY_LOCALE', () => this.parseString(raw.primaryLocale)) ?? undefined);
123
+ }
124
+ /** The settings.inherited.json view of the cloud's raw payload; undefined when nothing is inherited. */
125
+ async inheritedFromRaw(raw) {
126
+ const origins = this.originsOf(raw);
127
+ const inheritedFrom = {};
128
+ for (const [setting, field] of Object.entries(SETTING_FIELDS)) {
129
+ const { mode, from } = origins[setting];
130
+ if (mode === 'INHERIT' && from) {
131
+ inheritedFrom[field] = from;
132
+ }
133
+ }
134
+ if (Object.keys(inheritedFrom).length === 0) {
135
+ return undefined;
71
136
  }
72
- return new types_1.WorkspaceSettings(raw.slug, raw.parentTeams ?? [], raw.zoneId, errorHandlerConfig, this.parseWebhookAuth(raw.webhookAuth), raw.primaryLocale ?? undefined);
137
+ // JSON.stringify drops the settings left undefined, so the file lists only inherited ones.
138
+ return {
139
+ zoneId: inheritedFrom.zoneId ? this.parseString(raw.zoneId) : undefined,
140
+ errorHandlerConfig: inheritedFrom.errorHandlerConfig ? await this.resolveErrorHandler(raw) : undefined,
141
+ webhookAuth: inheritedFrom.webhookAuth ? this.parseWebhookAuth(raw.webhookAuth) : undefined,
142
+ primaryLocale: inheritedFrom.primaryLocale ? this.parseString(raw.primaryLocale) : undefined,
143
+ inheritedFrom,
144
+ };
145
+ }
146
+ /**
147
+ * settings.json is the source of truth for the team's own values: a setting absent from it is
148
+ * reset to inherit, null leaves the team with none (a parent's included), and a value sets it.
149
+ */
150
+ async toTeamUpdate(settings) {
151
+ const { errorHandlerConfig, webhookAuth } = settings;
152
+ return {
153
+ parentTeams: settings.parentTeams,
154
+ inheritableSettings: {
155
+ zoneId: ownOrInherit(settings.zoneId),
156
+ errorHandlerConfig: errorHandlerConfig === undefined || errorHandlerConfig === null
157
+ ? noneOrInherit(errorHandlerConfig)
158
+ : {
159
+ mode: 'OWN',
160
+ value: {
161
+ processId: await this.resolveSlugToId(errorHandlerConfig.processSlug),
162
+ tag: errorHandlerConfig.tag ?? null,
163
+ },
164
+ },
165
+ webhookAuth: webhookAuth === undefined || webhookAuth === null
166
+ ? noneOrInherit(webhookAuth)
167
+ : { mode: 'OWN', value: { headerName: webhookAuth.headerName ?? null, variableKey: webhookAuth.variableKey } },
168
+ primaryLocale: ownOrInherit(settings.primaryLocale),
169
+ },
170
+ };
73
171
  }
74
172
  /**
75
173
  * Writes the cloud's workspace versions and aliases into the settings as the settings.json
@@ -99,6 +197,25 @@ class SettingsRepository {
99
197
  const match = Object.values(processes).find((process) => process.id === processId);
100
198
  return match?.slug ?? processId;
101
199
  }
200
+ /** Where each setting comes from; an API without it predates settings inheritance. */
201
+ originsOf(raw) {
202
+ if (!raw.origins) {
203
+ throw new errors_1.PlatformPredatesSettingsInheritance();
204
+ }
205
+ return raw.origins;
206
+ }
207
+ async resolveErrorHandler(raw) {
208
+ if (!raw.errorHandlerConfig?.processId) {
209
+ return undefined;
210
+ }
211
+ return {
212
+ processSlug: await this.resolveIdToSlug(raw.errorHandlerConfig.processId),
213
+ tag: raw.errorHandlerConfig.tag ?? null,
214
+ };
215
+ }
216
+ parseString(raw) {
217
+ return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
218
+ }
102
219
  parseErrorHandler(raw) {
103
220
  if (!raw || !raw.processSlug) {
104
221
  return undefined;
@@ -194,7 +311,8 @@ class SettingsRepository {
194
311
  if (settings.errorHandlerConfig !== undefined) {
195
312
  file.errorHandlerConfig = settings.errorHandlerConfig;
196
313
  }
197
- // Last, so adding webhook auth to an existing settings.json is a purely additive diff.
314
+ // Last, so adding webhook auth to an existing settings.json is a purely additive diff. null
315
+ // (cleared) is written as is: absent means inherit.
198
316
  if (settings.webhookAuth !== undefined) {
199
317
  file.webhookAuth = settings.webhookAuth;
200
318
  }
@@ -88,9 +88,9 @@ declare class FcodeGraphQLClient {
88
88
  private mapFactorialSettings;
89
89
  mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug, lifecycleRole, deletedAt, retainingVersionTags, }: any): Process;
90
90
  /**
91
- * GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
92
- * keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
93
- * undefined.
91
+ * The team's inheritable settings, each read from its `{ mode, value, from }`: the value in effect
92
+ * and where it comes from. An API that predates settings inheritance has no such field, and leaves
93
+ * `origins` undefined so the settings commands refuse instead of reading every setting as inherited.
94
94
  */
95
95
  mapTeamSettings(team: any): RawTeamSettings;
96
96
  mapTeamVersion(teamVersion: any): TeamVersion;
@@ -105,6 +105,11 @@ declare class FcodeGraphQLClient {
105
105
  verifyTeamAccess(): Promise<void>;
106
106
  team(): Promise<RawTeamSettings>;
107
107
  pushTeam(update: TeamUpdateInput): Promise<RawTeamSettings>;
108
+ /**
109
+ * An API that predates settings inheritance rejects the query naming `inheritableSettings` as a
110
+ * schema error; that reads better as what it means than as GraphQL validation output.
111
+ */
112
+ cleanTeamSettingsError(error: any): any;
108
113
  mapProgrammingLanguageManifest(programmingLanguageManifest: any): ProgrammingLanguageManifest;
109
114
  private mapDependencies;
110
115
  /**
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const graphql_request_1 = require("graphql-request");
4
4
  const types_1 = require("../core/types");
5
5
  const locale_merger_1 = require("./locale-merger");
6
+ const cli_version_1 = require("../utils/cli-version");
7
+ const errors_1 = require("../core/errors");
6
8
  const _ = require("lodash");
7
9
  // Source code travels base64 encoded: Cloudflare's WAF blocks requests whose body
8
10
  // looks like an attack payload, which source code often does. sourceCodeEncoding tells the
@@ -137,6 +139,17 @@ const PROCESS_FIELDS = `
137
139
  }
138
140
  }
139
141
  `;
142
+ // Each inheritable setting of a team: its mode, the value in effect and the parent it comes from.
143
+ const INHERITABLE_SETTINGS_SELECTION = `
144
+ inheritableSettings {
145
+ zoneId { mode value from }
146
+ paramsSchemaValidationEnabled { mode value from }
147
+ errorHandlerConfig { mode value { processId tag } from }
148
+ webhookAuth { mode value { headerName variableKey } from }
149
+ primaryLocale { mode value from }
150
+ }
151
+ `;
152
+ const UNKNOWN_FIELD_ERROR = /FieldUndefined|WrongType|is undefined|not present|not in type/;
140
153
  const NETWORK_ERROR_CODES = ['ECONNREFUSED', 'ENOTFOUND', 'ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN'];
141
154
  class FcodeGraphQLClient {
142
155
  constructor(options) {
@@ -152,6 +165,7 @@ class FcodeGraphQLClient {
152
165
  headers: {
153
166
  'Fcode-Team': options.teamSlug,
154
167
  authorization: `Bearer ${options.accessToken}`,
168
+ [cli_version_1.CLI_VERSION_HEADER]: (0, cli_version_1.cliVersion)(),
155
169
  },
156
170
  });
157
171
  }
@@ -1005,10 +1019,9 @@ class FcodeGraphQLClient {
1005
1019
  metadata.webhook = webhook;
1006
1020
  }
1007
1021
  if (settings?.formSettings) {
1022
+ // `appRole` is retired and push refuses it, so it is not written back: a role the cloud
1023
+ // already holds stays there, and `factorial.form.appTool` carries the user-facing one.
1008
1024
  metadata.form = { enabled: Boolean(settings.formSettings.enabled) };
1009
- if (settings.formSettings.appRole && settings.formSettings.appRole !== 'NONE') {
1010
- metadata.form.appRole = settings.formSettings.appRole;
1011
- }
1012
1025
  if (settings.formSettings.authMode && settings.formSettings.authMode !== 'NONE') {
1013
1026
  metadata.form.authMode = settings.formSettings.authMode;
1014
1027
  }
@@ -1039,27 +1052,37 @@ class FcodeGraphQLClient {
1039
1052
  return process;
1040
1053
  }
1041
1054
  /**
1042
- * GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
1043
- * keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
1044
- * undefined.
1055
+ * The team's inheritable settings, each read from its `{ mode, value, from }`: the value in effect
1056
+ * and where it comes from. An API that predates settings inheritance has no such field, and leaves
1057
+ * `origins` undefined so the settings commands refuse instead of reading every setting as inherited.
1045
1058
  */
1046
1059
  mapTeamSettings(team) {
1047
1060
  const settings = {
1048
1061
  slug: team.slug,
1049
- zoneId: team.zoneId,
1050
- errorHandlerConfig: team.errorHandlerConfig,
1051
1062
  parentTeams: team.parentTeams ?? [],
1052
1063
  };
1053
- const webhookAuth = team.settings?.webhookAuth;
1064
+ const inheritable = team.inheritableSettings;
1065
+ if (!inheritable) {
1066
+ return settings;
1067
+ }
1068
+ settings.zoneId = inheritable.zoneId?.value ?? undefined;
1069
+ settings.errorHandlerConfig = inheritable.errorHandlerConfig?.value ?? null;
1070
+ settings.primaryLocale = inheritable.primaryLocale?.value ?? null;
1071
+ const webhookAuth = inheritable.webhookAuth?.value;
1054
1072
  if (webhookAuth?.variableKey) {
1055
1073
  settings.webhookAuth = { variableKey: webhookAuth.variableKey };
1056
1074
  if (webhookAuth.headerName && webhookAuth.headerName !== types_1.DEFAULT_WEBHOOK_AUTH_HEADER) {
1057
1075
  settings.webhookAuth.headerName = webhookAuth.headerName;
1058
1076
  }
1059
1077
  }
1060
- if (team.settings?.primaryLocale) {
1061
- settings.primaryLocale = team.settings.primaryLocale;
1062
- }
1078
+ const origin = (setting) => ({ mode: setting?.mode ?? 'INHERIT', from: setting?.from ?? null });
1079
+ settings.origins = {
1080
+ ZONE_ID: origin(inheritable.zoneId),
1081
+ PARAMS_SCHEMA_VALIDATION: origin(inheritable.paramsSchemaValidationEnabled),
1082
+ ERROR_HANDLER: origin(inheritable.errorHandlerConfig),
1083
+ WEBHOOK_AUTH: origin(inheritable.webhookAuth),
1084
+ PRIMARY_LOCALE: origin(inheritable.primaryLocale),
1085
+ };
1063
1086
  return settings;
1064
1087
  }
1065
1088
  mapTeamVersion(teamVersion) {
@@ -1195,28 +1218,17 @@ class FcodeGraphQLClient {
1195
1218
  query team($slug: ID!) {
1196
1219
  team(slug: $slug) {
1197
1220
  slug
1198
- zoneId
1199
- errorHandlerConfig {
1200
- processId
1201
- tag
1202
- }
1203
1221
  parentTeams {
1204
1222
  slug
1205
1223
  version
1206
1224
  }
1207
- settings {
1208
- webhookAuth {
1209
- headerName
1210
- variableKey
1211
- }
1212
- primaryLocale
1213
- }
1225
+ ${INHERITABLE_SETTINGS_SELECTION}
1214
1226
  }
1215
1227
  }
1216
1228
  `, { slug: this.teamSlug })
1217
1229
  .then(({ team }) => this.mapTeamSettings(team))
1218
1230
  .catch((error) => {
1219
- return Promise.reject(this.cleanError(error));
1231
+ return Promise.reject(this.cleanTeamSettingsError(error));
1220
1232
  });
1221
1233
  }
1222
1234
  pushTeam(update) {
@@ -1225,30 +1237,32 @@ class FcodeGraphQLClient {
1225
1237
  mutation updateTeam($slug: String!, $update: TeamUpdateInput!) {
1226
1238
  updateTeam(slug: $slug, update: $update) {
1227
1239
  slug
1228
- zoneId
1229
- errorHandlerConfig {
1230
- processId
1231
- tag
1232
- }
1233
1240
  parentTeams {
1234
1241
  slug
1235
1242
  version
1236
1243
  }
1237
- settings {
1238
- webhookAuth {
1239
- headerName
1240
- variableKey
1241
- }
1242
- primaryLocale
1243
- }
1244
+ ${INHERITABLE_SETTINGS_SELECTION}
1244
1245
  }
1245
1246
  }
1246
1247
  `, { slug: this.teamSlug, update })
1247
1248
  .then(({ updateTeam: team }) => this.mapTeamSettings(team))
1248
1249
  .catch((error) => {
1249
- return Promise.reject(this.cleanError(error));
1250
+ return Promise.reject(this.cleanTeamSettingsError(error));
1250
1251
  });
1251
1252
  }
1253
+ /**
1254
+ * An API that predates settings inheritance rejects the query naming `inheritableSettings` as a
1255
+ * schema error; that reads better as what it means than as GraphQL validation output.
1256
+ */
1257
+ cleanTeamSettingsError(error) {
1258
+ const message = error?.response?.errors?.[0]?.message ?? '';
1259
+ // Only the schema errors an older API answers with (an unknown output field, an unknown input
1260
+ // field): the API's own validation errors also name inheritableSettings, and mean something else.
1261
+ if (message.includes('inheritableSettings') && UNKNOWN_FIELD_ERROR.test(message)) {
1262
+ return new errors_1.PlatformPredatesSettingsInheritance();
1263
+ }
1264
+ return this.cleanError(error);
1265
+ }
1252
1266
  mapProgrammingLanguageManifest(programmingLanguageManifest) {
1253
1267
  const allDependencies = programmingLanguageManifest.nextInstallation?.dependencies || programmingLanguageManifest.dependencies;
1254
1268
  return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.programmingLanguage, programmingLanguageManifest.programmingLanguage, this.mapDependencies(allDependencies),
@@ -71,16 +71,20 @@ function readLocale(workspaceDir, locale) {
71
71
  ...readLocaleFile(workspaceDir, locale),
72
72
  };
73
73
  }
74
- function chosenPrimaryLocaleOf(workspaceDir) {
74
+ function primaryLocaleIn(workspaceDir, fileName) {
75
75
  try {
76
76
  (0, settings_1.migrateLegacySettingsFile)(workspaceDir);
77
- const settings = fs.readJsonSync(path.join(workspaceDir, settings_1.SETTINGS_FILENAME));
77
+ const settings = fs.readJsonSync(path.join(workspaceDir, fileName));
78
78
  return typeof settings?.primaryLocale === 'string' && settings.primaryLocale.length > 0 ? settings.primaryLocale : undefined;
79
79
  }
80
80
  catch {
81
81
  return undefined;
82
82
  }
83
83
  }
84
+ /** The workspace's own choice, or the one it inherits when settings.json makes none. */
85
+ function chosenPrimaryLocaleOf(workspaceDir) {
86
+ return primaryLocaleIn(workspaceDir, settings_1.SETTINGS_FILENAME) ?? primaryLocaleIn(workspaceDir, settings_1.INHERITED_SETTINGS_FILENAME);
87
+ }
84
88
  /** Every locale file in the workspace, own and inherited: `pull` writes them all to `i18n/`. */
85
89
  function localeFileSlugs(workspaceDir) {
86
90
  const directory = path.join(workspaceDir, I18N_DIRECTORY);
@@ -0,0 +1,15 @@
1
+ import { Process } from '../core/types';
2
+ /**
3
+ * Refuses to push a process that still declares `form.appRole`.
4
+ *
5
+ * The role is retired: a lifecycle form is named by its reserved slug, and the form offered from
6
+ * the app's page is `factorial.form.appTool`. Writing the role kept agents copying it into new
7
+ * Apps, where it beats the slug and, set by `appTool`, took the install role away. A process that
8
+ * already holds a role in the cloud keeps it when the line is deleted, since a push only sends the
9
+ * role when metadata.json names it — so legacy Apps need no rename to push again.
10
+ *
11
+ * Inherited and deleted processes are not pushed, so they are not checked.
12
+ *
13
+ * @returns the message to fail the push with, or undefined when no process declares a role.
14
+ */
15
+ export declare function retiredAppRoleError(processes: Array<Process>): string | undefined;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.retiredAppRoleError = retiredAppRoleError;
4
+ const types_1 = require("../core/types");
5
+ const LIFECYCLE_SLUGS = ['install', 'settings', 'uninstall'];
6
+ /**
7
+ * Refuses to push a process that still declares `form.appRole`.
8
+ *
9
+ * The role is retired: a lifecycle form is named by its reserved slug, and the form offered from
10
+ * the app's page is `factorial.form.appTool`. Writing the role kept agents copying it into new
11
+ * Apps, where it beats the slug and, set by `appTool`, took the install role away. A process that
12
+ * already holds a role in the cloud keeps it when the line is deleted, since a push only sends the
13
+ * role when metadata.json names it — so legacy Apps need no rename to push again.
14
+ *
15
+ * Inherited and deleted processes are not pushed, so they are not checked.
16
+ *
17
+ * @returns the message to fail the push with, or undefined when no process declares a role.
18
+ */
19
+ function retiredAppRoleError(processes) {
20
+ const offenders = processes.filter((process) => !(0, types_1.isReadOnly)(process) && process.form?.appRole && process.form.appRole !== 'NONE');
21
+ if (offenders.length === 0) {
22
+ return undefined;
23
+ }
24
+ const lines = offenders.map((process) => ` - processes/${process.slug}: ${fixFor(process)}`);
25
+ return [
26
+ '`form.appRole` is no longer supported. Remove it from metadata.json:',
27
+ ...lines,
28
+ `Install, settings and uninstall forms are the processes named ${LIFECYCLE_SLUGS.map((slug) => `'${slug}'`).join(', ')}.`,
29
+ ].join('\n');
30
+ }
31
+ function fixFor(process) {
32
+ const role = process.form.appRole;
33
+ if (role === 'USER_FACING_FORM') {
34
+ return 'set `factorial.form.appTool: true` instead.';
35
+ }
36
+ const slug = role.toLowerCase();
37
+ if (process.slug === slug) {
38
+ return `the '${slug}' slug already makes it the ${slug} form.`;
39
+ }
40
+ if (process.id) {
41
+ return `the cloud keeps the ${role} role it already has once the line is gone.`;
42
+ }
43
+ return `rename the process to '${slug}'.`;
44
+ }
@@ -13,9 +13,11 @@ export type WebhookAuthResult = {
13
13
  */
14
14
  export declare function findHeader(headers: http.IncomingHttpHeaders, name: string): string | undefined;
15
15
  /**
16
- * The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM.
16
+ * The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM. The
17
+ * workspace's is its own (settings.json) or, when settings.json leaves it out, the inherited one
18
+ * (settings.inherited.json); null in settings.json clears it, a parent's included.
17
19
  * Returns null only when the webhook is public — TEAM without a workspace configuration throws,
18
20
  * because treating it as public would silently unprotect a webhook its owner chose to protect.
19
21
  */
20
- export declare function resolveEffectiveAuth(webhook: ProcessWebhook | undefined, teamWebhookAuth: WebhookAuth | undefined): WebhookAuth | null;
22
+ export declare function resolveEffectiveAuth(webhook: ProcessWebhook | undefined, teamWebhookAuth: WebhookAuth | null | undefined, inheritedTeamWebhookAuth?: WebhookAuth): WebhookAuth | null;
21
23
  export declare function checkWebhookAuth(effectiveAuth: WebhookAuth | null, headers: http.IncomingHttpHeaders, variableValue: string | undefined): WebhookAuthResult;
@@ -26,20 +26,26 @@ function findHeader(headers, name) {
26
26
  return undefined;
27
27
  }
28
28
  /**
29
- * The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM.
29
+ * The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM. The
30
+ * workspace's is its own (settings.json) or, when settings.json leaves it out, the inherited one
31
+ * (settings.inherited.json); null in settings.json clears it, a parent's included.
30
32
  * Returns null only when the webhook is public — TEAM without a workspace configuration throws,
31
33
  * because treating it as public would silently unprotect a webhook its owner chose to protect.
32
34
  */
33
- function resolveEffectiveAuth(webhook, teamWebhookAuth) {
35
+ function resolveEffectiveAuth(webhook, teamWebhookAuth, inheritedTeamWebhookAuth) {
34
36
  const authMode = webhook?.authMode;
35
37
  if (!authMode || authMode === 'NONE') {
36
38
  return null;
37
39
  }
38
40
  if (authMode === 'TEAM') {
39
- if (!teamWebhookAuth) {
40
- throw new Error('this webhook inherits the workspace webhook auth, which is not set in settings.json');
41
+ if (teamWebhookAuth === null) {
42
+ throw new Error('this webhook inherits the workspace webhook auth, which settings.json sets to none');
41
43
  }
42
- return teamWebhookAuth;
44
+ const workspaceAuth = teamWebhookAuth ?? inheritedTeamWebhookAuth;
45
+ if (!workspaceAuth) {
46
+ throw new Error('this webhook inherits the workspace webhook auth, which is not set in settings.json nor inherited');
47
+ }
48
+ return workspaceAuth;
43
49
  }
44
50
  if (!webhook?.auth?.variableKey) {
45
51
  throw new Error('this webhook uses the CUSTOM auth mode but names no variable in metadata.json');
@@ -0,0 +1,7 @@
1
+ /** The header the CLI reports its version in, so the platform can refuse a CLI too old for a request. */
2
+ export declare const CLI_VERSION_HEADER = "Fcode-Cli-Version";
3
+ /**
4
+ * The installed CLI's own version, read from its package.json: two levels above this file both in
5
+ * src/utils (tests) and dist/utils (the published package, which always ships package.json).
6
+ */
7
+ export declare function cliVersion(): string;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLI_VERSION_HEADER = void 0;
4
+ exports.cliVersion = cliVersion;
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ /** The header the CLI reports its version in, so the platform can refuse a CLI too old for a request. */
8
+ exports.CLI_VERSION_HEADER = 'Fcode-Cli-Version';
9
+ let cached;
10
+ /**
11
+ * The installed CLI's own version, read from its package.json: two levels above this file both in
12
+ * src/utils (tests) and dist/utils (the published package, which always ships package.json).
13
+ */
14
+ function cliVersion() {
15
+ if (cached === undefined) {
16
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8'));
17
+ cached = String(packageJson.version);
18
+ }
19
+ return cached;
20
+ }
@@ -16,6 +16,7 @@ export declare function renderGitignoreWithInherited(existing: string, inherited
16
16
  * current workspace metadata, so resources that stop being inherited drop out.
17
17
  * Inherited variable values live in one file, which is ignored as soon as the
18
18
  * workspace inherits any variable; inherited locales are one file each, like modules;
19
- * inherited dependencies are one file per language, like variables.
19
+ * inherited dependencies are one file per language, like variables; inherited settings are one
20
+ * file.
20
21
  */
21
22
  export declare function syncInheritedGitignore(workspaceDir?: string): Promise<void>;
@@ -9,6 +9,7 @@ const workspace_1 = require("../config/workspace");
9
9
  const global_1 = require("../config/global");
10
10
  const settings_1 = require("../config/settings");
11
11
  const programming_language_manifest_1 = require("../repositories/programming-language-manifest");
12
+ const settings_2 = require("../repositories/settings");
12
13
  const filenamify_1 = require("./filenamify");
13
14
  const file_utils_1 = require("./file-utils");
14
15
  exports.INHERITED_BLOCK_START = '# BEGIN fcode inherited resources (auto-generated, do not edit)';
@@ -51,7 +52,8 @@ function stripManagedBlock(content) {
51
52
  * current workspace metadata, so resources that stop being inherited drop out.
52
53
  * Inherited variable values live in one file, which is ignored as soon as the
53
54
  * workspace inherits any variable; inherited locales are one file each, like modules;
54
- * inherited dependencies are one file per language, like variables.
55
+ * inherited dependencies are one file per language, like variables; inherited settings are one
56
+ * file.
55
57
  */
56
58
  async function syncInheritedGitignore(workspaceDir = process.cwd()) {
57
59
  const team = await new workspace_1.default(workspaceDir).getActiveTeam();
@@ -71,6 +73,8 @@ async function syncInheritedGitignore(workspaceDir = process.cwd()) {
71
73
  // inherit — checked on disk because the manifest metadata tracks the workspace's own list, not
72
74
  // what its parents contribute.
73
75
  ...exports.INHERITED_DEPENDENCY_FILES.filter((file) => fs.existsSync(path.join(workspaceDir, file))),
76
+ // Written only when the workspace inherits a setting, so checked on disk like dependencies.
77
+ ...(fs.existsSync(path.join(workspaceDir, settings_2.INHERITED_SETTINGS_FILENAME)) ? [settings_2.INHERITED_SETTINGS_FILENAME] : []),
74
78
  ];
75
79
  const gitignorePath = path.join(workspaceDir, '.gitignore');
76
80
  const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
@@ -2516,5 +2516,5 @@
2516
2516
  ]
2517
2517
  }
2518
2518
  },
2519
- "version": "3.5.1"
2519
+ "version": "3.6.1"
2520
2520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorialco/fcode-cli",
3
- "version": "3.5.1",
3
+ "version": "3.6.1",
4
4
  "packageManager": "pnpm@11.7.0",
5
5
  "description": "Factorial Code Command Line Interface",
6
6
  "homepage": "https://code.factorialhr.com/docs/cli/",