@noodleseed/one 0.172.1 → 0.173.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.
Files changed (48) hide show
  1. package/dist/commands/solutions-draft-source.d.ts +9 -0
  2. package/dist/commands/solutions-draft-source.d.ts.map +1 -0
  3. package/dist/commands/solutions-draft-source.js +67 -0
  4. package/dist/commands/solutions-draft-source.js.map +1 -0
  5. package/dist/commands/solutions-drafts.d.ts +5 -0
  6. package/dist/commands/solutions-drafts.d.ts.map +1 -0
  7. package/dist/commands/solutions-drafts.js +159 -0
  8. package/dist/commands/solutions-drafts.js.map +1 -0
  9. package/dist/commands/solutions-ops.d.ts.map +1 -1
  10. package/dist/commands/solutions-ops.js +7 -1
  11. package/dist/commands/solutions-ops.js.map +1 -1
  12. package/dist/commands/solutions-page.d.ts +5 -0
  13. package/dist/commands/solutions-page.d.ts.map +1 -0
  14. package/dist/commands/solutions-page.js +85 -0
  15. package/dist/commands/solutions-page.js.map +1 -0
  16. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-drafts.js +141 -0
  17. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-page.js +57 -0
  18. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +4 -0
  19. package/node_modules/@noodle-borg/service/dist/application-drafts/contracts.js +11 -0
  20. package/node_modules/@noodle-borg/service/dist/application-drafts/paths.js +26 -0
  21. package/node_modules/@noodle-borg/service/dist/business-information/business-page-paths.js +14 -0
  22. package/node_modules/@noodle-borg/service/dist/business-information/business-page.d.ts +39 -0
  23. package/node_modules/@noodle-borg/service/dist/business-information/business-page.js +81 -0
  24. package/node_modules/@noodle-borg/service/dist/business-information/contracts.d.ts +2 -0
  25. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-business-pages.js +37 -0
  26. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +3 -0
  27. package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-executable.js +11 -3
  28. package/node_modules/@noodle-borg/service/dist/routes/application-drafts.js +162 -0
  29. package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +3 -0
  30. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +22 -0
  31. package/node_modules/@noodle-borg/service/dist/routes/business-information-pages.js +158 -0
  32. package/node_modules/@noodle-borg/service/dist/routes/service-probes.js +31 -0
  33. package/node_modules/@noodle-borg/service/dist/service.js +14 -18
  34. package/node_modules/@noodle-borg/service/dist/solution-installation-activation.js +3 -3
  35. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.d.ts +3 -0
  36. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.js +1 -0
  37. package/node_modules/@noodle-borg/wire-contracts/dist/application-drafts.d.ts +322 -0
  38. package/node_modules/@noodle-borg/wire-contracts/dist/application-drafts.js +152 -0
  39. package/node_modules/@noodle-borg/wire-contracts/dist/billing-org-transfer.d.ts +8 -8
  40. package/node_modules/@noodle-borg/wire-contracts/dist/billing-read.d.ts +5 -5
  41. package/node_modules/@noodle-borg/wire-contracts/dist/business-page.d.ts +157 -0
  42. package/node_modules/@noodle-borg/wire-contracts/dist/business-page.js +105 -0
  43. package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.d.ts +22 -0
  44. package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.js +15 -0
  45. package/node_modules/@noodle-borg/wire-contracts/dist/distribution.d.ts +10 -10
  46. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +3 -0
  47. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +3 -0
  48. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ /** Canonical source-draft operations; imported by the solutions family. Data only. */
2
+ import { EXPECTED_REVISION, IDEMPOTENCY_KEY, OPTIONAL_FLAG, REQUIRED_ARGUMENT, SOLUTION_COMMON_FLAGS, } from './catalog-data-solution-flags.js';
3
+ const common = [
4
+ ...SOLUTION_COMMON_FLAGS.map((flag) => flag.name === 'org' ? { ...flag, required: true } : flag),
5
+ {
6
+ ...OPTIONAL_FLAG,
7
+ name: 'app',
8
+ type: 'string',
9
+ value: '<slug>',
10
+ required: true,
11
+ summary: 'Application slug.',
12
+ },
13
+ ];
14
+ const source = [
15
+ {
16
+ ...OPTIONAL_FLAG,
17
+ name: 'source-dir',
18
+ type: 'string',
19
+ value: '<directory>',
20
+ required: true,
21
+ summary: 'Directory containing bounded TypeScript application source; no symlinks or environment files.',
22
+ },
23
+ {
24
+ ...OPTIONAL_FLAG,
25
+ name: 'entrypoint',
26
+ type: 'string',
27
+ value: '<path>',
28
+ summary: 'Relative TypeScript entrypoint.',
29
+ constraints: { default: 'server.ts' },
30
+ },
31
+ ];
32
+ const draftId = { ...REQUIRED_ARGUMENT, name: 'draft-id', summary: 'Saved draft identifier.' };
33
+ const revision = { ...EXPECTED_REVISION, constraints: { minimum: 1, maximum: 2147483647 } };
34
+ export const SOLUTION_DRAFTS = {
35
+ name: 'drafts',
36
+ summary: 'Save and inspect private application source without publishing it.',
37
+ arguments: [],
38
+ flags: [],
39
+ subcommands: [
40
+ {
41
+ name: 'history',
42
+ summary: 'List source-free revisions, newest first.',
43
+ arguments: [draftId],
44
+ flags: common,
45
+ jsonOutput: { mode: 'single' },
46
+ },
47
+ {
48
+ name: 'diff',
49
+ summary: 'Compare exact source and entrypoints at two revisions.',
50
+ arguments: [draftId],
51
+ flags: [
52
+ ...common,
53
+ { ...revision, name: 'from-revision', summary: 'Revision to compare from.' },
54
+ { ...revision, name: 'to-revision', summary: 'Revision to compare to.' },
55
+ ],
56
+ jsonOutput: { mode: 'single' },
57
+ },
58
+ {
59
+ name: 'list',
60
+ summary: 'List source-free draft summaries.',
61
+ arguments: [],
62
+ flags: common,
63
+ jsonOutput: { mode: 'single' },
64
+ },
65
+ {
66
+ name: 'show',
67
+ summary: 'Read an authorized exact draft revision.',
68
+ arguments: [draftId],
69
+ flags: [
70
+ ...common,
71
+ {
72
+ ...revision,
73
+ name: 'revision',
74
+ required: false,
75
+ summary: 'Revision to read; defaults to the current revision.',
76
+ },
77
+ ],
78
+ jsonOutput: { mode: 'single' },
79
+ },
80
+ {
81
+ name: 'create',
82
+ summary: 'Save a new private draft from TypeScript source.',
83
+ arguments: [],
84
+ flags: [
85
+ ...common,
86
+ ...source,
87
+ IDEMPOTENCY_KEY,
88
+ {
89
+ ...OPTIONAL_FLAG,
90
+ name: 'env',
91
+ type: 'string',
92
+ value: '<slug>',
93
+ summary: 'Intended publish environment; saving does not deploy.',
94
+ constraints: { default: 'prod' },
95
+ },
96
+ ],
97
+ jsonOutput: { mode: 'single' },
98
+ },
99
+ {
100
+ name: 'edit',
101
+ summary: 'Append a revision without overwriting concurrent work.',
102
+ arguments: [draftId],
103
+ flags: [...common, ...source, revision, IDEMPOTENCY_KEY],
104
+ jsonOutput: { mode: 'single' },
105
+ },
106
+ {
107
+ name: 'undo',
108
+ summary: 'Restore an earlier source revision as a new revision.',
109
+ arguments: [draftId],
110
+ flags: [
111
+ ...common,
112
+ revision,
113
+ IDEMPOTENCY_KEY,
114
+ {
115
+ ...revision,
116
+ name: 'target-revision',
117
+ summary: 'Earlier revision whose source will be restored.',
118
+ },
119
+ ],
120
+ jsonOutput: { mode: 'single' },
121
+ },
122
+ {
123
+ name: 'delete',
124
+ summary: 'Erase a draft and its source history; published applications are unchanged.',
125
+ arguments: [draftId],
126
+ flags: [
127
+ ...common,
128
+ revision,
129
+ {
130
+ ...OPTIONAL_FLAG,
131
+ name: 'confirm',
132
+ type: 'boolean',
133
+ required: true,
134
+ summary: 'Confirm erasing the draft source and history.',
135
+ },
136
+ ],
137
+ jsonOutput: { mode: 'single' },
138
+ },
139
+ ],
140
+ };
141
+ //# sourceMappingURL=catalog-data-solution-drafts.js.map
@@ -0,0 +1,57 @@
1
+ /** Hosted-page operator catalog. Pure data; execution remains in the CLI. */
2
+ import { EXPECTED_REVISION, INSTALLATION_ARGUMENT, OPTIONAL_FLAG, SOLUTION_COMMON_FLAGS, } from './catalog-data-solution-flags.js';
3
+ const common = SOLUTION_COMMON_FLAGS.map((flag) => flag.name === 'org' ? { ...flag, required: true } : flag);
4
+ const revision = {
5
+ ...EXPECTED_REVISION,
6
+ constraints: { minimum: 1, maximum: 2147483647 },
7
+ };
8
+ const confirm = {
9
+ ...OPTIONAL_FLAG,
10
+ name: 'confirm',
11
+ type: 'boolean',
12
+ required: true,
13
+ summary: 'Confirm changing public page availability.',
14
+ };
15
+ export const SOLUTION_PAGE = {
16
+ name: 'page',
17
+ summary: 'Edit and explicitly publish a Noodle Seed-hosted business page.',
18
+ arguments: [],
19
+ flags: [],
20
+ subcommands: [
21
+ {
22
+ name: 'show',
23
+ summary: 'Inspect the private page draft and its published snapshot.',
24
+ arguments: [INSTALLATION_ARGUMENT],
25
+ flags: common,
26
+ jsonOutput: { mode: 'single' },
27
+ },
28
+ {
29
+ name: 'save',
30
+ summary: 'Save page content without changing the published page.',
31
+ arguments: [INSTALLATION_ARGUMENT],
32
+ flags: [
33
+ ...common,
34
+ { ...revision, constraints: { minimum: 0, maximum: 2147483647 } },
35
+ {
36
+ ...OPTIONAL_FLAG,
37
+ name: 'data',
38
+ type: 'string',
39
+ value: '<json>',
40
+ required: true,
41
+ summary: 'Bounded JSON with introduction and sections containing title/text; plain text only.',
42
+ },
43
+ ],
44
+ jsonOutput: { mode: 'single' },
45
+ },
46
+ ...['publish', 'unpublish'].map((name) => ({
47
+ name,
48
+ summary: name === 'publish'
49
+ ? 'Publish the exact saved page after public-assistant readiness checks.'
50
+ : 'Remove the public page while retaining its private draft.',
51
+ arguments: [INSTALLATION_ARGUMENT],
52
+ flags: [...common, revision, confirm],
53
+ jsonOutput: { mode: 'single' },
54
+ })),
55
+ ],
56
+ };
57
+ //# sourceMappingURL=catalog-data-solution-page.js.map
@@ -1,5 +1,7 @@
1
1
  /** Solution installation/operator catalog. Imports only dependency-free sibling data. */
2
+ import { SOLUTION_DRAFTS } from './catalog-data-solution-drafts.js';
2
3
  import { AUTH_TOKEN, EXPECTED_REVISION, IDEMPOTENCY_KEY, INSTALLATION_ARGUMENT, JSON_FLAG, OPTIONAL_FLAG, PAGING_FLAGS, REQUIRED_ARGUMENT, SERVICE, SOLUTION_COMMON_FLAGS, } from './catalog-data-solution-flags.js';
4
+ import { SOLUTION_PAGE } from './catalog-data-solution-page.js';
3
5
  const APP = {
4
6
  ...OPTIONAL_FLAG,
5
7
  name: 'app',
@@ -129,6 +131,8 @@ export const CATALOG_SOLUTIONS = {
129
131
  arguments: [],
130
132
  flags: [],
131
133
  subcommands: [
134
+ SOLUTION_DRAFTS,
135
+ SOLUTION_PAGE,
132
136
  {
133
137
  name: 'installation-options',
134
138
  summary: 'List organizations currently authorized to install solutions.',
@@ -0,0 +1,11 @@
1
+ export class ApplicationDraftError extends Error {
2
+ code;
3
+ currentRevision;
4
+ constructor(code, currentRevision) {
5
+ super(code);
6
+ this.code = code;
7
+ this.currentRevision = currentRevision;
8
+ this.name = 'ApplicationDraftError';
9
+ }
10
+ }
11
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1,26 @@
1
+ import { validateSlug } from '@noodle-borg/control-plane/portable';
2
+ import { ApplicationDraftIdSchema } from '@noodle-borg/wire-contracts';
3
+ export function parseApplicationDraftPath(path) {
4
+ const match = /^\/v1\/orgs\/([a-z0-9-]+)\/apps\/([a-z0-9-]+)\/drafts(?:\/([a-f0-9-]+)(?:\/(undo|history|diff))?)?$/.exec(path);
5
+ const org = match?.[1], app = match?.[2], id = match?.[3], action = match?.[4];
6
+ if (!org || !app || (id !== undefined && !ApplicationDraftIdSchema.safeParse(id).success))
7
+ return undefined;
8
+ try {
9
+ validateSlug('org', org);
10
+ validateSlug('app', app);
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ return { org, app, ...(id ? { id } : {}), ...(action ? { action } : {}) };
16
+ }
17
+ export function applicationDraftMethods(ref) {
18
+ return ref.action === 'undo'
19
+ ? ['POST']
20
+ : ref.action
21
+ ? ['GET']
22
+ : ref.id
23
+ ? ['GET', 'PATCH', 'DELETE']
24
+ : ['GET', 'POST'];
25
+ }
26
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,14 @@
1
+ export function parseBusinessPagePath(path) {
2
+ const match = /^\/v1\/orgs\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)\/solution-installations\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)\/page(?:\/(publish|unpublish))?$/.exec(path);
3
+ return match
4
+ ? {
5
+ org: match[1],
6
+ installationId: match[2],
7
+ ...(match[3] ? { operation: match[3] } : {}),
8
+ }
9
+ : undefined;
10
+ }
11
+ export function businessPageMethods(ref) {
12
+ return ref.operation ? ['POST'] : ['GET', 'PUT'];
13
+ }
14
+ //# sourceMappingURL=business-page-paths.js.map
@@ -0,0 +1,39 @@
1
+ import { type BusinessPageContent, type BusinessPagePublication } from '@noodle-borg/wire-contracts';
2
+ import type { BusinessNoticeRecord } from './business-notice.js';
3
+ import type { InstallationScope } from './contracts.js';
4
+ export interface BusinessPageRecord {
5
+ readonly revision: number;
6
+ readonly draft: BusinessPageContent;
7
+ readonly published: BusinessPagePublication | null;
8
+ readonly updatedAt: string;
9
+ readonly updatedBySubject: string;
10
+ }
11
+ export type BusinessPageChange = {
12
+ readonly operation: 'save';
13
+ readonly expectedRevision: number;
14
+ readonly content: BusinessPageContent;
15
+ } | {
16
+ readonly operation: 'publish' | 'unpublish';
17
+ readonly expectedRevision: number;
18
+ };
19
+ export interface BusinessPageInput {
20
+ readonly scope: InstallationScope;
21
+ readonly actorSubject: string;
22
+ readonly change: BusinessPageChange;
23
+ }
24
+ /** Server-only readiness check, never supplied by a browser or persisted as authority. */
25
+ export type BusinessPageReadiness = () => Promise<{
26
+ readonly deploymentId: string;
27
+ }>;
28
+ export interface BusinessPageStore {
29
+ get(scope: InstallationScope): Promise<BusinessPageRecord | undefined>;
30
+ update(input: BusinessPageInput, assertReady?: BusinessPageReadiness): Promise<BusinessPageRecord>;
31
+ }
32
+ export declare class BusinessPageError extends Error {
33
+ readonly code: 'business_page_forbidden' | 'business_page_conflict' | 'business_page_not_ready';
34
+ constructor(code: 'business_page_forbidden' | 'business_page_conflict' | 'business_page_not_ready');
35
+ }
36
+ export declare function nextBusinessPage(current: BusinessPageRecord | undefined, input: BusinessPageInput, now: string, notice: BusinessNoticeRecord | undefined, assertReady?: BusinessPageReadiness): Promise<BusinessPageRecord>;
37
+ /** Validate decrypted storage without surfacing parser excerpts or business text in errors. */
38
+ export declare function decodeBusinessPage(value: unknown, expectedRevision: number): BusinessPageRecord;
39
+ //# sourceMappingURL=business-page.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { BusinessPageContentSchema, BusinessPagePublicationSchema, BusinessPagePublishRequestSchema, BusinessPageSaveRequestSchema, } from '@noodle-borg/wire-contracts';
2
+ export class BusinessPageError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super({
6
+ business_page_forbidden: 'An active business administrator is required.',
7
+ business_page_conflict: 'The page changed. Reload before trying again.',
8
+ business_page_not_ready: 'Complete the business details and enable the public assistant before publishing.',
9
+ }[code]);
10
+ this.code = code;
11
+ this.name = 'BusinessPageError';
12
+ }
13
+ }
14
+ export async function nextBusinessPage(current, input, now, notice, assertReady) {
15
+ const { operation, ...request } = input.change;
16
+ if (!['save', 'publish', 'unpublish'].includes(operation))
17
+ throw new Error('Invalid page operation.');
18
+ const save = operation === 'save' ? BusinessPageSaveRequestSchema.parse(request) : undefined;
19
+ const expectedRevision = save?.expectedRevision ?? BusinessPagePublishRequestSchema.parse(request).expectedRevision;
20
+ if ((current?.revision ?? 0) !== expectedRevision || expectedRevision === 2_147_483_647)
21
+ throw new BusinessPageError('business_page_conflict');
22
+ if (operation !== 'save' && !current)
23
+ throw new BusinessPageError('business_page_conflict');
24
+ const draft = save?.content ?? current?.draft;
25
+ if (!draft)
26
+ throw new BusinessPageError('business_page_conflict');
27
+ let published = current?.published ?? null;
28
+ if (operation === 'publish') {
29
+ if (!notice || !assertReady)
30
+ throw new BusinessPageError('business_page_not_ready');
31
+ const ready = await assertReady();
32
+ published = BusinessPagePublicationSchema.parse({
33
+ content: draft,
34
+ notice: notice.notice,
35
+ noticeRevision: notice.revision,
36
+ sourceRevision: expectedRevision,
37
+ deploymentId: ready.deploymentId,
38
+ publishedAt: now,
39
+ });
40
+ }
41
+ else if (operation === 'unpublish')
42
+ published = null;
43
+ return structuredClone({
44
+ revision: expectedRevision + 1,
45
+ draft,
46
+ published,
47
+ updatedAt: now,
48
+ updatedBySubject: input.actorSubject,
49
+ });
50
+ }
51
+ /** Validate decrypted storage without surfacing parser excerpts or business text in errors. */
52
+ export function decodeBusinessPage(value, expectedRevision) {
53
+ try {
54
+ if (!value || typeof value !== 'object' || Array.isArray(value))
55
+ throw new Error();
56
+ const input = value;
57
+ if (Object.keys(input).sort().join(',') !==
58
+ 'draft,published,revision,updatedAt,updatedBySubject' ||
59
+ input.revision !== expectedRevision ||
60
+ !Number.isInteger(expectedRevision) ||
61
+ expectedRevision < 1 ||
62
+ expectedRevision > 2_147_483_647 ||
63
+ typeof input.updatedAt !== 'string' ||
64
+ !Number.isFinite(Date.parse(input.updatedAt)) ||
65
+ typeof input.updatedBySubject !== 'string' ||
66
+ !input.updatedBySubject ||
67
+ input.updatedBySubject.length > 500)
68
+ throw new Error();
69
+ return {
70
+ revision: expectedRevision,
71
+ draft: BusinessPageContentSchema.parse(input.draft),
72
+ published: input.published === null ? null : BusinessPagePublicationSchema.parse(input.published),
73
+ updatedAt: input.updatedAt,
74
+ updatedBySubject: input.updatedBySubject,
75
+ };
76
+ }
77
+ catch {
78
+ throw new Error('Business page storage is unavailable.');
79
+ }
80
+ }
81
+ //# sourceMappingURL=business-page.js.map
@@ -1,6 +1,7 @@
1
1
  import type { ArtifactVariableDeclaration, ManagedCollectionControls } from '@noodle-borg/compiler';
2
2
  import type { ManagedRecordQuery } from '@noodle-borg/wire-contracts';
3
3
  import type { BusinessNoticeStore } from './business-notice.js';
4
+ import type { BusinessPageStore } from './business-page.js';
4
5
  import type { BusinessPrincipalProvider } from './principal-authority.js';
5
6
  export type JsonPrimitive = string | number | boolean | null;
6
7
  export type JsonValue = JsonPrimitive | JsonObject | readonly JsonValue[];
@@ -433,6 +434,7 @@ export interface ManagedRequestStore {
433
434
  }): Promise<number>;
434
435
  }
435
436
  export interface BusinessInformationStore extends SolutionInstallationStore, BusinessNoticeStore, BusinessGrantStore, ManagedRequestStore {
437
+ readonly pages: BusinessPageStore;
436
438
  configurePrincipalAuthority(provider: BusinessPrincipalProvider | undefined): void;
437
439
  listEligibleAssignees(scope: InstallationScope): Promise<readonly BusinessGrant[]>;
438
440
  }
@@ -0,0 +1,37 @@
1
+ import { BusinessPageError, nextBusinessPage, } from './business-page.js';
2
+ import { scopeKey } from './pagination.js';
3
+ import { validateScalar, validateScope } from './validation.js';
4
+ /** Process-local test/development adapter; shares installation grant authority and its locks. */
5
+ export class InMemoryBusinessPages {
6
+ business;
7
+ locks;
8
+ principals;
9
+ now;
10
+ #records = new Map();
11
+ constructor(business, locks, principals, now) {
12
+ this.business = business;
13
+ this.locks = locks;
14
+ this.principals = principals;
15
+ this.now = now;
16
+ }
17
+ async get(scope) {
18
+ return structuredClone(this.#records.get(scopeKey(validateScope(scope))));
19
+ }
20
+ async update(input, assertReady) {
21
+ const scope = validateScope(input.scope);
22
+ const actorSubject = validateScalar('page actor', input.actorSubject, 500);
23
+ const key = scopeKey(scope);
24
+ return this.locks.run(`grants:${key}`, () => this.locks.run(`intake:${key}`, async () => {
25
+ const grant = await this.business.getGrant(scope, actorSubject);
26
+ if (!(await this.business.getInstallation(scope)) ||
27
+ grant?.role !== 'administrator' ||
28
+ grant.revokedAt ||
29
+ !(await this.principals.allows(actorSubject)))
30
+ throw new BusinessPageError('business_page_forbidden');
31
+ const next = await nextBusinessPage(this.#records.get(key), { ...input, scope, actorSubject }, this.now().toISOString(), await this.business.getBusinessNotice(scope), assertReady);
32
+ this.#records.set(key, structuredClone(next));
33
+ return next;
34
+ }));
35
+ }
36
+ }
37
+ //# sourceMappingURL=in-memory-business-pages.js.map
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { memoryActivityPage } from './activity-pagination.js';
3
3
  import { collectionControlEnabled } from './collection-controls.js';
4
+ import { InMemoryBusinessPages } from './in-memory-business-pages.js';
4
5
  import { InMemoryInstallationLifecycle, } from './in-memory-installation-lifecycle.js';
5
6
  import { InMemoryBusinessInvitations } from './in-memory-invitations.js';
6
7
  import { BusinessMemoryLocks } from './in-memory-locks.js';
@@ -14,6 +15,7 @@ import { BusinessPrincipalAuthority, } from './principal-authority.js';
14
15
  import { boundedExportPageSize, boundedPageSize, validateEmail, validateScalar, validateScope, } from './validation.js';
15
16
  /** Process-local development/test adapter. Hosted services must use the PostgreSQL adapter. */
16
17
  export class InMemoryBusinessInformationStore {
18
+ pages;
17
19
  #installations = new Map();
18
20
  #installationIds = new Map();
19
21
  #publicIds = new Map();
@@ -36,6 +38,7 @@ export class InMemoryBusinessInformationStore {
36
38
  this.#id = options.id ?? randomUUID;
37
39
  this.#publicId = options.publicId ?? (() => `sol_${randomUUID().replaceAll('-', '')}`);
38
40
  this.#managedDefinition = options.managedDefinition;
41
+ this.pages = new InMemoryBusinessPages(this, this.#locks, this.#principals, this.#now);
39
42
  this.#lifecycle = new InMemoryInstallationLifecycle({
40
43
  installations: this.#installations,
41
44
  getGrant: (scope, subject) => this.#grants.get(grantKey(scope, subject)),
@@ -2,7 +2,15 @@ import { projectManagedCollectionControls, RECORD_OPERATION_SIGNATURES, } from '
2
2
  import { collectionToPublicWire } from '../routes/business-information-wire.js';
3
3
  import { managedSolutionBlueprint } from './managed-solution-blueprints.js';
4
4
  /** System-owned manifest data for an explicitly curated solution, admitted by the ordinary compiler. */
5
- export function managedSolutionManifest(definition) {
5
+ export function managedSolutionManifest(definition, hostedPageOrigin) {
6
+ if (hostedPageOrigin !== undefined) {
7
+ const url = new URL(hostedPageOrigin);
8
+ if (url.origin !== hostedPageOrigin ||
9
+ !(url.protocol === 'https:' ||
10
+ (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))))
11
+ throw new Error('Invalid hosted page origin');
12
+ }
13
+ const origins = ['${env.WEBSITE_ORIGIN}', ...(hostedPageOrigin ? [hostedPageOrigin] : [])];
6
14
  const ref = definition.reference;
7
15
  const blueprint = ref.kind === 'managed' ? managedSolutionBlueprint(ref.definitionId, ref.release) : undefined;
8
16
  if (blueprint === undefined)
@@ -40,11 +48,11 @@ export function managedSolutionManifest(definition) {
40
48
  })),
41
49
  assistant: {
42
50
  model: { kind: 'noodle-managed' },
43
- allowedOrigins: ['${env.WEBSITE_ORIGIN}'],
51
+ allowedOrigins: origins,
44
52
  surfaces: [
45
53
  {
46
54
  mode: 'public',
47
- origins: ['${env.WEBSITE_ORIGIN}'],
55
+ origins,
48
56
  capabilities: [{ kind: 'tool', name: blueprint.tool }],
49
57
  },
50
58
  ],
@@ -0,0 +1,162 @@
1
+ import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
2
+ import { APPLICATION_DRAFT_LIMITS, ApplicationDraftCreateRequestSchema, ApplicationDraftDiffRequestSchema, ApplicationDraftDiffResponseSchema, ApplicationDraftEditRequestSchema, ApplicationDraftHistoryResponseSchema, ApplicationDraftListResponseSchema, ApplicationDraftResponseSchema, ApplicationDraftRevisionRequestSchema, ApplicationDraftUndoRequestSchema, } from '@noodle-borg/wire-contracts';
3
+ import { ApplicationDraftError } from '../application-drafts/contracts.js';
4
+ import { applicationDraftMethods, parseApplicationDraftPath } from '../application-drafts/paths.js';
5
+ import { admitBusinessTarget, authorizeBusinessApi } from '../business-api-admission.js';
6
+ import { requestIdempotencyKey } from './business-information-request.js';
7
+ /** This bounded authoring surface never forwards a Portal token to general deployment/secrets APIs. */
8
+ export async function handleApplicationDraftRoute(req, res, url, deps) {
9
+ res.setHeader('cache-control', 'private, no-store');
10
+ try {
11
+ await handle(req, res, url, deps);
12
+ }
13
+ catch (error) {
14
+ if (error instanceof ApplicationDraftError) {
15
+ const status = error.code === 'forbidden'
16
+ ? 403
17
+ : error.code === 'not_found'
18
+ ? 404
19
+ : error.code === 'draft_deleted'
20
+ ? 410
21
+ : error.code === 'invalid_draft'
22
+ ? 400
23
+ : 409;
24
+ return sendJson(res, status, {
25
+ error: error.code.replaceAll('_', ' '),
26
+ code: error.code,
27
+ ...(error.currentRevision === undefined ? {} : { currentRevision: error.currentRevision }),
28
+ });
29
+ }
30
+ // Do not reflect compiler/source/cipher errors or log their customer-controlled messages.
31
+ return sendJson(res, 503, {
32
+ error: 'Draft storage is temporarily unavailable.',
33
+ code: 'drafts_unavailable',
34
+ });
35
+ }
36
+ }
37
+ async function handle(req, res, url, deps) {
38
+ const ref = parseApplicationDraftPath(url.pathname);
39
+ if (!ref)
40
+ return sendJson(res, 404, { error: 'draft route not found' });
41
+ const { org, app, id, action } = ref;
42
+ const identity = await authorizeBusinessApi(req, res, deps);
43
+ if (!identity)
44
+ return;
45
+ const permission = req.method === 'GET' ? 'drafts:read' : 'drafts:edit';
46
+ if ((await deps.workspaces.authorize(org, identity.subject, permission)) !== 'allowed') {
47
+ return sendJson(res, 403, { error: 'Workspace draft permission required.', code: 'forbidden' });
48
+ }
49
+ if (!(await admitBusinessTarget(res, { org })))
50
+ return;
51
+ const scope = { org, app }, actorSubject = identity.subject;
52
+ if (id && req.method === 'GET' && action === 'history') {
53
+ if ([...url.searchParams].length)
54
+ return invalid(res);
55
+ const revisions = await deps.drafts.history(scope, id, actorSubject);
56
+ return sendJson(res, 200, ApplicationDraftHistoryResponseSchema.parse({ ok: true, data: { revisions } }));
57
+ }
58
+ if (id && req.method === 'GET' && action === 'diff') {
59
+ if ([...url.searchParams].length !== 2 ||
60
+ [...url.searchParams].some(([key, value]) => !['from', 'to'].includes(key) || !/^[1-9]\d{0,9}$/.test(value)))
61
+ return invalid(res);
62
+ const parsed = ApplicationDraftDiffRequestSchema.safeParse({
63
+ from: Number(url.searchParams.get('from')),
64
+ to: Number(url.searchParams.get('to')),
65
+ });
66
+ if (!parsed.success)
67
+ return invalid(res);
68
+ const diff = await deps.drafts.diff(scope, id, actorSubject, parsed.data.from, parsed.data.to);
69
+ return sendJson(res, 200, ApplicationDraftDiffResponseSchema.parse({ ok: true, data: { diff } }));
70
+ }
71
+ if (req.method === 'GET' && action === undefined) {
72
+ if (id === undefined) {
73
+ if ([...url.searchParams].length)
74
+ return invalid(res);
75
+ const drafts = await deps.drafts.list(scope, actorSubject);
76
+ return sendJson(res, 200, ApplicationDraftListResponseSchema.parse({ ok: true, data: { drafts } }));
77
+ }
78
+ const queries = [...url.searchParams];
79
+ const value = url.searchParams.get('revision');
80
+ if (queries.length > 1 ||
81
+ queries.some(([key]) => key !== 'revision') ||
82
+ (value !== null && (!/^[1-9]\d{0,9}$/.test(value) || Number(value) > 2147483647)))
83
+ return invalid(res);
84
+ const draft = await deps.drafts.get(scope, id, actorSubject, value === null ? undefined : Number(value));
85
+ return sendJson(res, 200, ApplicationDraftResponseSchema.parse({ ok: true, data: { draft } }));
86
+ }
87
+ const allowed = applicationDraftMethods(ref);
88
+ if (!allowed.includes(req.method ?? '')) {
89
+ res.setHeader('allow', allowed.join(', '));
90
+ return sendJson(res, 405, { error: 'method not allowed' });
91
+ }
92
+ if ([...url.searchParams].length)
93
+ return invalid(res);
94
+ const body = await readJsonBody(req, Math.min(deps.maxBody, APPLICATION_DRAFT_LIMITS.totalBytes * 6 + 32_768));
95
+ if (!body.ok)
96
+ return sendJson(res, body.status, { error: body.error });
97
+ if (id !== undefined && req.method === 'DELETE') {
98
+ const parsed = ApplicationDraftRevisionRequestSchema.safeParse(body.value);
99
+ if (!parsed.success)
100
+ return invalid(res);
101
+ await deps.drafts.remove({
102
+ scope,
103
+ id,
104
+ actorSubject,
105
+ expectedRevision: parsed.data.expectedRevision,
106
+ });
107
+ res.writeHead(204);
108
+ res.end();
109
+ return;
110
+ }
111
+ const idempotencyKey = requestIdempotencyKey(req);
112
+ if (!idempotencyKey)
113
+ return invalid(res);
114
+ if (id === undefined && req.method === 'POST') {
115
+ const parsed = ApplicationDraftCreateRequestSchema.safeParse(body.value);
116
+ if (!parsed.success)
117
+ return invalid(res);
118
+ const draft = await deps.drafts.create({
119
+ scope,
120
+ actorSubject,
121
+ idempotencyKey,
122
+ environment: parsed.data.environment,
123
+ source: parsed.data.source,
124
+ ...(parsed.data.baseRelease === undefined ? {} : { baseRelease: parsed.data.baseRelease }),
125
+ });
126
+ return sendJson(res, 201, ApplicationDraftResponseSchema.parse({ ok: true, data: { draft } }));
127
+ }
128
+ if (id && req.method === 'PATCH') {
129
+ const parsed = ApplicationDraftEditRequestSchema.safeParse(body.value);
130
+ if (!parsed.success)
131
+ return invalid(res);
132
+ const draft = await deps.drafts.edit({
133
+ scope,
134
+ id,
135
+ actorSubject,
136
+ idempotencyKey,
137
+ ...parsed.data,
138
+ });
139
+ return sendJson(res, 200, ApplicationDraftResponseSchema.parse({ ok: true, data: { draft } }));
140
+ }
141
+ if (id && action === 'undo' && req.method === 'POST') {
142
+ const parsed = ApplicationDraftUndoRequestSchema.safeParse(body.value);
143
+ if (!parsed.success)
144
+ return invalid(res);
145
+ const draft = await deps.drafts.undo({
146
+ scope,
147
+ id,
148
+ actorSubject,
149
+ idempotencyKey,
150
+ ...parsed.data,
151
+ });
152
+ return sendJson(res, 200, ApplicationDraftResponseSchema.parse({ ok: true, data: { draft } }));
153
+ }
154
+ return invalid(res);
155
+ }
156
+ function invalid(res) {
157
+ sendJson(res, 400, {
158
+ error: 'Invalid draft request. Review the source, revision and retry key.',
159
+ code: 'invalid_draft',
160
+ });
161
+ }
162
+ //# sourceMappingURL=application-drafts.js.map