@noodleseed/one 0.172.0 → 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.
- package/dist/commands/solutions-draft-source.d.ts +9 -0
- package/dist/commands/solutions-draft-source.d.ts.map +1 -0
- package/dist/commands/solutions-draft-source.js +67 -0
- package/dist/commands/solutions-draft-source.js.map +1 -0
- package/dist/commands/solutions-drafts.d.ts +5 -0
- package/dist/commands/solutions-drafts.d.ts.map +1 -0
- package/dist/commands/solutions-drafts.js +159 -0
- package/dist/commands/solutions-drafts.js.map +1 -0
- package/dist/commands/solutions-ops.d.ts.map +1 -1
- package/dist/commands/solutions-ops.js +7 -1
- package/dist/commands/solutions-ops.js.map +1 -1
- package/dist/commands/solutions-page.d.ts +5 -0
- package/dist/commands/solutions-page.d.ts.map +1 -0
- package/dist/commands/solutions-page.js +85 -0
- package/dist/commands/solutions-page.js.map +1 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-drafts.js +141 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-page.js +57 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +4 -0
- package/node_modules/@noodle-borg/service/dist/application-drafts/contracts.js +11 -0
- package/node_modules/@noodle-borg/service/dist/application-drafts/paths.js +26 -0
- package/node_modules/@noodle-borg/service/dist/business-information/business-page-paths.js +14 -0
- package/node_modules/@noodle-borg/service/dist/business-information/business-page.d.ts +39 -0
- package/node_modules/@noodle-borg/service/dist/business-information/business-page.js +81 -0
- package/node_modules/@noodle-borg/service/dist/business-information/contracts.d.ts +2 -0
- package/node_modules/@noodle-borg/service/dist/business-information/in-memory-business-pages.js +37 -0
- package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +3 -0
- package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-executable.js +11 -3
- package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +7 -2
- package/node_modules/@noodle-borg/service/dist/channels/runtime.js +4 -3
- package/node_modules/@noodle-borg/service/dist/routes/application-drafts.js +162 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +3 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +22 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-information-pages.js +158 -0
- package/node_modules/@noodle-borg/service/dist/routes/service-probes.js +31 -0
- package/node_modules/@noodle-borg/service/dist/service.js +14 -18
- package/node_modules/@noodle-borg/service/dist/solution-installation-activation.js +3 -3
- package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.d.ts +3 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.js +1 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/application-drafts.d.ts +322 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/application-drafts.js +152 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/billing-org-transfer.d.ts +8 -8
- package/node_modules/@noodle-borg/wire-contracts/dist/billing-read.d.ts +5 -5
- package/node_modules/@noodle-borg/wire-contracts/dist/business-page.d.ts +157 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/business-page.js +105 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.d.ts +22 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.js +15 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/distribution.d.ts +10 -10
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +3 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +3 -0
- 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
|
}
|
package/node_modules/@noodle-borg/service/dist/business-information/in-memory-business-pages.js
ADDED
|
@@ -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)),
|
package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-executable.js
CHANGED
|
@@ -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:
|
|
51
|
+
allowedOrigins: origins,
|
|
44
52
|
surfaces: [
|
|
45
53
|
{
|
|
46
54
|
mode: 'public',
|
|
47
|
-
origins
|
|
55
|
+
origins,
|
|
48
56
|
capabilities: [{ kind: 'tool', name: blueprint.tool }],
|
|
49
57
|
},
|
|
50
58
|
],
|
|
@@ -131,13 +131,18 @@ export class Dialog360 {
|
|
|
131
131
|
if (!response.ok)
|
|
132
132
|
throw new ChannelError('provider_unavailable');
|
|
133
133
|
const parsed = z
|
|
134
|
-
.object({
|
|
134
|
+
.object({
|
|
135
|
+
id: identifier,
|
|
136
|
+
health_status: z.object({ can_send_message: z.enum(['AVAILABLE', 'LIMITED', 'BLOCKED']) }),
|
|
137
|
+
})
|
|
135
138
|
.safeParse(await boundedJson(response));
|
|
136
139
|
if (!parsed.success)
|
|
137
140
|
throw new ChannelError('provider_response_invalid');
|
|
138
141
|
return {
|
|
139
142
|
phoneNumberId: parsed.data.id,
|
|
140
|
-
|
|
143
|
+
// LIMITED meets provider messaging requirements; provider limits still govern each send.
|
|
144
|
+
canSend: parsed.data.health_status.can_send_message !== 'BLOCKED',
|
|
145
|
+
status: parsed.data.health_status.can_send_message,
|
|
141
146
|
};
|
|
142
147
|
}
|
|
143
148
|
async webhook() {
|
|
@@ -56,8 +56,8 @@ export class WhatsAppRuntime {
|
|
|
56
56
|
const checks = [];
|
|
57
57
|
const check = async (name, work) => {
|
|
58
58
|
try {
|
|
59
|
-
await work();
|
|
60
|
-
checks.push({ name, status: 'ready' });
|
|
59
|
+
const code = await work();
|
|
60
|
+
checks.push({ name, status: 'ready', ...(code ? { code } : {}) });
|
|
61
61
|
}
|
|
62
62
|
catch (error) {
|
|
63
63
|
checks.push({
|
|
@@ -98,7 +98,8 @@ export class WhatsAppRuntime {
|
|
|
98
98
|
if (health.phoneNumberId !== binding.phoneNumberId)
|
|
99
99
|
throw new ChannelError('asset_mismatch');
|
|
100
100
|
if (!health.canSend)
|
|
101
|
-
throw new ChannelError('
|
|
101
|
+
throw new ChannelError('provider_messaging_blocked');
|
|
102
|
+
return health.status === 'LIMITED' ? 'provider_messaging_limited' : undefined;
|
|
102
103
|
});
|
|
103
104
|
await check('webhook', async () => {
|
|
104
105
|
const state = await this.webhook(id, req);
|