@noodleseed/one 0.172.1 → 0.174.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-args.d.ts +42 -0
- package/dist/commands/solutions-ops-args.d.ts.map +1 -0
- package/dist/commands/solutions-ops-args.js +47 -0
- package/dist/commands/solutions-ops-args.js.map +1 -0
- package/dist/commands/solutions-ops.d.ts.map +1 -1
- package/dist/commands/solutions-ops.js +12 -46
- 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/dist/commands/solutions-workspace.d.ts +5 -0
- package/dist/commands/solutions-workspace.d.ts.map +1 -0
- package/dist/commands/solutions-workspace.js +132 -0
- package/dist/commands/solutions-workspace.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-solution-workspace.js +88 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +6 -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/business-workspaces/contracts.js +52 -0
- package/node_modules/@noodle-borg/service/dist/business-workspaces/paths.js +17 -0
- package/node_modules/@noodle-borg/service/dist/routes/application-drafts.js +162 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-authoring-dispatch.js +23 -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 +16 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-information-pages.js +158 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-workspaces.js +106 -0
- package/node_modules/@noodle-borg/service/dist/routes/service-probes.js +32 -0
- package/node_modules/@noodle-borg/service/dist/service.js +20 -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 +210 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.js +102 -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,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
|
],
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SLUG_PATTERN, validateSlug } from '@noodle-borg/control-plane/portable';
|
|
2
|
+
import { BusinessWorkspaceRoleSchema } from '@noodle-borg/wire-contracts';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export const WorkspaceSubjectSchema = z.string().min(1).max(500);
|
|
5
|
+
export const WorkspaceOrgSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(SLUG_PATTERN)
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
try {
|
|
10
|
+
validateSlug('org', value);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
export const WorkspaceRevisionSchema = z.number().int().positive().max(2147483647);
|
|
18
|
+
export const WorkspaceMemberSchema = z.strictObject({
|
|
19
|
+
subject: WorkspaceSubjectSchema,
|
|
20
|
+
role: BusinessWorkspaceRoleSchema,
|
|
21
|
+
joinedAt: z.iso.datetime(),
|
|
22
|
+
});
|
|
23
|
+
const invitation = z.strictObject({
|
|
24
|
+
id: z.string().uuid(),
|
|
25
|
+
email: z.email().max(254),
|
|
26
|
+
role: BusinessWorkspaceRoleSchema,
|
|
27
|
+
tokenDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
28
|
+
createdBy: WorkspaceSubjectSchema,
|
|
29
|
+
createdAt: z.iso.datetime(),
|
|
30
|
+
expiresAt: z.iso.datetime(),
|
|
31
|
+
});
|
|
32
|
+
/** Internal encrypted state. Never serialize pending invitation material into a staff response. */
|
|
33
|
+
export const WorkspaceStateSchema = z.strictObject({
|
|
34
|
+
org: WorkspaceOrgSchema,
|
|
35
|
+
authorityVersion: z.literal(1),
|
|
36
|
+
revision: WorkspaceRevisionSchema,
|
|
37
|
+
initializedBy: WorkspaceSubjectSchema,
|
|
38
|
+
activatedAt: z.iso.datetime(),
|
|
39
|
+
members: z.array(WorkspaceMemberSchema).min(1).max(1000),
|
|
40
|
+
invitations: z.array(invitation).max(100),
|
|
41
|
+
});
|
|
42
|
+
export class BusinessWorkspaceError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
currentRevision;
|
|
45
|
+
constructor(code, currentRevision) {
|
|
46
|
+
super(code);
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.currentRevision = currentRevision;
|
|
49
|
+
this.name = 'BusinessWorkspaceError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const root = /^\/v1\/orgs\/([a-z0-9][a-z0-9-]{0,62})\/business-workspace(?:\/(members|accept|invitations)(?:\/([a-f0-9-]{36}))?)?$/;
|
|
2
|
+
export function parseBusinessWorkspacePath(path) {
|
|
3
|
+
const match = root.exec(path);
|
|
4
|
+
if (!match?.[1] || (match[3] && match[2] !== 'invitations'))
|
|
5
|
+
return undefined;
|
|
6
|
+
return { org: match[1], action: match[2], id: match[3] };
|
|
7
|
+
}
|
|
8
|
+
export function businessWorkspaceMethod(ref) {
|
|
9
|
+
if (!ref.action)
|
|
10
|
+
return 'GET';
|
|
11
|
+
if (ref.action === 'members')
|
|
12
|
+
return 'PATCH';
|
|
13
|
+
if (ref.action === 'invitations' && ref.id)
|
|
14
|
+
return 'DELETE';
|
|
15
|
+
return 'POST';
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { applySecurityHeaders, enforceHttps } from '@noodle-borg/transport-http';
|
|
2
|
+
import { parseApplicationDraftPath } from '../application-drafts/paths.js';
|
|
3
|
+
import { parseBusinessWorkspacePath } from '../business-workspaces/paths.js';
|
|
4
|
+
import { handleApplicationDraftRoute, } from './application-drafts.js';
|
|
5
|
+
import { handleBusinessWorkspaceRoute } from './business-workspaces.js';
|
|
6
|
+
/** Explicit versioned authority, independent of the managed-record switch. */
|
|
7
|
+
export function dispatchBusinessAuthoringRoutes(req, res, url, authoring, deps) {
|
|
8
|
+
if (!authoring)
|
|
9
|
+
return false;
|
|
10
|
+
const workspace = parseBusinessWorkspacePath(url.pathname);
|
|
11
|
+
if (!workspace && !parseApplicationDraftPath(url.pathname))
|
|
12
|
+
return false;
|
|
13
|
+
applySecurityHeaders(res, deps.tls);
|
|
14
|
+
if (!enforceHttps(req, res, deps.tls)) {
|
|
15
|
+
const merged = { ...deps, ...authoring };
|
|
16
|
+
if (workspace)
|
|
17
|
+
void handleBusinessWorkspaceRoute(req, res, url, merged);
|
|
18
|
+
else
|
|
19
|
+
void handleApplicationDraftRoute(req, res, url, merged);
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=business-authoring-dispatch.js.map
|
|
@@ -64,6 +64,9 @@ export async function projectBusinessChannels(installation, canEdit, base, deps)
|
|
|
64
64
|
revision: installation.revision,
|
|
65
65
|
active: installation.intakeActive,
|
|
66
66
|
canEdit,
|
|
67
|
+
...(deps.pageOrigin
|
|
68
|
+
? { businessPageUrl: `${deps.pageOrigin}/b/${installation.publicId}` }
|
|
69
|
+
: {}),
|
|
67
70
|
...(whatsapp
|
|
68
71
|
? {
|
|
69
72
|
whatsapp: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OrganizationAgreementError } from '@noodle-borg/control-plane/portable';
|
|
2
2
|
import { sendJson } from '@noodle-borg/transport-http';
|
|
3
3
|
import { BusinessNoticeError } from '../business-information/business-notice.js';
|
|
4
|
+
import { parseBusinessPagePath } from '../business-information/business-page-paths.js';
|
|
4
5
|
import { InstallationCapacityError } from '../business-information/installation-capacity.js';
|
|
5
6
|
import { NativeStorageLimitError } from '../business-information/native-storage-budget.js';
|
|
6
7
|
import { SourceCredentialError } from '../business-information/source-credential-fence.js';
|
|
@@ -12,6 +13,7 @@ import { handleBusinessChannels, } from './business-information-channels.js';
|
|
|
12
13
|
import { handleApplicationConnectionCallback, handleApplicationConnections, } from './business-information-connections.js';
|
|
13
14
|
import { handleBusinessNotice, handleOrganizationAgreement, } from './business-information-notice.js';
|
|
14
15
|
import { handleSolutionInstallationOptions } from './business-information-onboarding.js';
|
|
16
|
+
import { handleBusinessPage, handlePublicBusinessPage } from './business-information-pages.js';
|
|
15
17
|
import { parsePublicSolutionIntakePath, parseSolutionInstallationPath, parseSolutionInvitationAcceptPath, } from './business-information-paths.js';
|
|
16
18
|
import { handlePublicSolutionIntake } from './business-information-public.js';
|
|
17
19
|
import { handleBusinessInformationReaderFloor } from './business-information-reader-floor.js';
|
|
@@ -19,6 +21,20 @@ import { handleBusinessSettings } from './business-information-settings.js';
|
|
|
19
21
|
import { handleCollectionSource } from './business-information-source.js';
|
|
20
22
|
import { handleBusinessInvitationAccept, handleBusinessInvitations, handleEligibleBusinessAssignees, handleMySolutionInstallations, } from './business-information-staff.js';
|
|
21
23
|
export function dispatchBusinessInformationRoutes(req, res, url, deps) {
|
|
24
|
+
const pageRef = parseBusinessPagePath(url.pathname);
|
|
25
|
+
const publicPage = /^\/v1\/business-pages\/(sol_[a-f0-9]{32})$/.exec(url.pathname);
|
|
26
|
+
if (pageRef || publicPage) {
|
|
27
|
+
if (!deps.pageOrigin || !deps.pageServiceUrl || !deps.registry || !deps.resolveEndpointBase)
|
|
28
|
+
return run(req, res, deps, () => Promise.resolve(sendJson(res, 404, { error: 'not found' })));
|
|
29
|
+
const pageDeps = {
|
|
30
|
+
...deps,
|
|
31
|
+
registry: deps.registry,
|
|
32
|
+
resolveEndpointBase: deps.resolveEndpointBase,
|
|
33
|
+
};
|
|
34
|
+
return run(req, res, deps, () => pageRef
|
|
35
|
+
? handleBusinessPage(req, res, pageRef, pageDeps)
|
|
36
|
+
: handlePublicBusinessPage(req, res, publicPage[1], pageDeps));
|
|
37
|
+
}
|
|
22
38
|
const agreement = /^\/v1\/orgs\/([a-z0-9][a-z0-9-]{0,62})\/agreement$/.exec(url.pathname);
|
|
23
39
|
if (agreement?.[1])
|
|
24
40
|
return run(req, res, deps, () => handleOrganizationAgreement(req, res, agreement[1], deps));
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
|
|
2
|
+
import { BusinessPagePublishRequestSchema, BusinessPageResponseSchema, BusinessPageSaveRequestSchema, PublicBusinessPageResponseSchema, } from '@noodle-borg/wire-contracts';
|
|
3
|
+
import { BusinessPageError, } from '../business-information/business-page.js';
|
|
4
|
+
import { businessPageMethods, } from '../business-information/business-page-paths.js';
|
|
5
|
+
import { requireIdentity, requireInstallationPermission } from './business-information.js';
|
|
6
|
+
import { projectBusinessChannels, } from './business-information-channels.js';
|
|
7
|
+
/** Publication custody remains valid independently of temporary assistant availability. */
|
|
8
|
+
async function pageContext(scope, deps) {
|
|
9
|
+
const installation = await deps.store.getInstallation(scope);
|
|
10
|
+
const generation = await deps.registry.getAppGeneration(scope.org, scope.app);
|
|
11
|
+
if (!installation ||
|
|
12
|
+
!generation ||
|
|
13
|
+
installation.applicationGeneration !== generation ||
|
|
14
|
+
(await deps.registry.getAppArchivedAt(scope.org, scope.app)) !== undefined ||
|
|
15
|
+
!deps.pageOrigin ||
|
|
16
|
+
!deps.pageServiceUrl ||
|
|
17
|
+
(deps.businessOnboarding && !(await deps.businessOnboarding.ready(installation))))
|
|
18
|
+
throw new BusinessPageError('business_page_not_ready');
|
|
19
|
+
const origin = new URL(deps.pageOrigin);
|
|
20
|
+
if (origin.origin !== deps.pageOrigin ||
|
|
21
|
+
!(origin.protocol === 'https:' ||
|
|
22
|
+
(origin.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(origin.hostname))))
|
|
23
|
+
throw new BusinessPageError('business_page_not_ready');
|
|
24
|
+
return { installation, origin: origin.origin, serviceUrl: deps.pageServiceUrl };
|
|
25
|
+
}
|
|
26
|
+
/** Only a current, explicitly configured public assistant can be attached to a hosted page. */
|
|
27
|
+
async function pageReadiness(scope, deps) {
|
|
28
|
+
const { installation, origin, serviceUrl } = await pageContext(scope, deps);
|
|
29
|
+
if (!installation.intakeActive || deps.publicIntakeEnabled === false)
|
|
30
|
+
throw new BusinessPageError('business_page_not_ready');
|
|
31
|
+
const channels = await projectBusinessChannels(installation, false, serviceUrl, deps);
|
|
32
|
+
const assistant = channels.assistant;
|
|
33
|
+
if (assistant.status !== 'ready' ||
|
|
34
|
+
!assistant.origins.includes(origin) ||
|
|
35
|
+
!channels.deploymentId ||
|
|
36
|
+
!assistant.embedId ||
|
|
37
|
+
!assistant.serviceUrl ||
|
|
38
|
+
!assistant.scriptUrl)
|
|
39
|
+
throw new BusinessPageError('business_page_not_ready');
|
|
40
|
+
return {
|
|
41
|
+
deploymentId: channels.deploymentId,
|
|
42
|
+
assistant: {
|
|
43
|
+
embedId: assistant.embedId,
|
|
44
|
+
serviceUrl: assistant.serviceUrl,
|
|
45
|
+
scriptUrl: assistant.scriptUrl,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function handleBusinessPage(req, res, ref, deps) {
|
|
50
|
+
res.setHeader('cache-control', 'private, no-store');
|
|
51
|
+
if (!businessPageMethods(ref).includes(req.method ?? '')) {
|
|
52
|
+
res.setHeader('allow', businessPageMethods(ref).join(', '));
|
|
53
|
+
return sendJson(res, 405, { error: 'method not allowed' });
|
|
54
|
+
}
|
|
55
|
+
const identity = await requireIdentity(req, res, deps);
|
|
56
|
+
if (identity === false)
|
|
57
|
+
return;
|
|
58
|
+
const authorized = await requireInstallationPermission(res, ref, identity, 'installation:administer', deps);
|
|
59
|
+
if (!authorized)
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
let record = await deps.store.pages.get(authorized.scope);
|
|
63
|
+
if (req.method !== 'GET') {
|
|
64
|
+
const body = await readJsonBody(req, Math.min(deps.maxBody, 256 * 1024));
|
|
65
|
+
if (!body.ok)
|
|
66
|
+
return sendJson(res, body.status, { error: body.error });
|
|
67
|
+
let change;
|
|
68
|
+
if (ref.operation) {
|
|
69
|
+
const parsed = BusinessPagePublishRequestSchema.safeParse(body.value);
|
|
70
|
+
if (!parsed.success)
|
|
71
|
+
return invalidPage(res);
|
|
72
|
+
change = { operation: ref.operation, ...parsed.data };
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const parsed = BusinessPageSaveRequestSchema.safeParse(body.value);
|
|
76
|
+
if (!parsed.success)
|
|
77
|
+
return invalidPage(res);
|
|
78
|
+
change = { operation: 'save', ...parsed.data };
|
|
79
|
+
}
|
|
80
|
+
record = await deps.store.pages.update({ scope: authorized.scope, actorSubject: identity.subject, change }, () => pageReadiness(authorized.scope, deps));
|
|
81
|
+
}
|
|
82
|
+
return sendJson(res, 200, BusinessPageResponseSchema.parse({
|
|
83
|
+
ok: true,
|
|
84
|
+
data: {
|
|
85
|
+
revision: record?.revision ?? 0,
|
|
86
|
+
draft: record?.draft ?? null,
|
|
87
|
+
published: record?.published ?? null,
|
|
88
|
+
canEdit: true,
|
|
89
|
+
},
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return pageFailure(res, error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function handlePublicBusinessPage(req, res, publicId, deps) {
|
|
97
|
+
res.setHeader('cache-control', 'no-store');
|
|
98
|
+
res.setHeader('x-robots-tag', 'noindex');
|
|
99
|
+
if (req.method !== 'GET') {
|
|
100
|
+
res.setHeader('allow', 'GET');
|
|
101
|
+
return sendJson(res, 405, { error: 'method not allowed' });
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const installation = await deps.store.resolveInstallationByPublicId(publicId);
|
|
105
|
+
if (!installation)
|
|
106
|
+
return unavailablePage(res);
|
|
107
|
+
const record = await deps.store.pages.get(installation.scope);
|
|
108
|
+
if (!record?.published)
|
|
109
|
+
return unavailablePage(res);
|
|
110
|
+
await pageContext(installation.scope, deps);
|
|
111
|
+
const notice = await deps.store.getBusinessNotice(installation.scope);
|
|
112
|
+
if (notice?.revision !== record.published.noticeRevision)
|
|
113
|
+
return unavailablePage(res);
|
|
114
|
+
const ready = await pageReadiness(installation.scope, deps).catch((error) => {
|
|
115
|
+
if (error instanceof BusinessPageError)
|
|
116
|
+
return undefined;
|
|
117
|
+
throw error;
|
|
118
|
+
});
|
|
119
|
+
return sendJson(res, 200, PublicBusinessPageResponseSchema.parse({
|
|
120
|
+
ok: true,
|
|
121
|
+
data: {
|
|
122
|
+
publicId,
|
|
123
|
+
content: record.published.content,
|
|
124
|
+
notice: record.published.notice,
|
|
125
|
+
assistant: ready?.deploymentId === record.published.deploymentId ? ready.assistant : null,
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error instanceof BusinessPageError)
|
|
131
|
+
return unavailablePage(res);
|
|
132
|
+
return pageFailure(res, error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function unavailablePage(res) {
|
|
136
|
+
sendJson(res, 404, { code: 'page_unavailable', error: 'This page is not available.' });
|
|
137
|
+
}
|
|
138
|
+
function invalidPage(res) {
|
|
139
|
+
sendJson(res, 400, {
|
|
140
|
+
code: 'business_page_invalid',
|
|
141
|
+
error: 'Check the page content and revision.',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function pageFailure(res, error) {
|
|
145
|
+
if (error instanceof BusinessPageError) {
|
|
146
|
+
sendJson(res, error.code === 'business_page_forbidden' ? 403 : 409, {
|
|
147
|
+
code: error.code,
|
|
148
|
+
error: error.message,
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Neither decrypted content nor provider/parser exceptions may escape via generic route logging.
|
|
153
|
+
sendJson(res, 503, {
|
|
154
|
+
code: 'page_unavailable',
|
|
155
|
+
error: 'The page service is temporarily unavailable.',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=business-information-pages.js.map
|