@owlmeans/viable-common 0.0.45 → 0.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -13
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/agent-presentation/SKILL.md +1 -1
- package/agent-meta/skills/viable-common/SKILL.md +132 -23
- package/build/connect/account/types.d.ts +81 -0
- package/build/connect/account/types.d.ts.map +1 -0
- package/build/connect/account/types.js +2 -0
- package/build/connect/account/types.js.map +1 -0
- package/build/connect/branding/types.d.ts +26 -4
- package/build/connect/branding/types.d.ts.map +1 -1
- package/build/connect/call/types.d.ts +28 -0
- package/build/connect/call/types.d.ts.map +1 -0
- package/build/connect/call/types.js +2 -0
- package/build/connect/call/types.js.map +1 -0
- package/build/connect/config/types.d.ts +45 -0
- package/build/connect/config/types.d.ts.map +1 -0
- package/build/connect/config/types.js +2 -0
- package/build/connect/config/types.js.map +1 -0
- package/build/connect/consts.d.ts +383 -1
- package/build/connect/consts.d.ts.map +1 -1
- package/build/connect/consts.js +387 -1
- package/build/connect/consts.js.map +1 -1
- package/build/connect/conversion/types.d.ts +24 -0
- package/build/connect/conversion/types.d.ts.map +1 -1
- package/build/connect/entrypoints/types.d.ts +22 -13
- package/build/connect/entrypoints/types.d.ts.map +1 -1
- package/build/connect/entrypoints.d.ts +441 -8
- package/build/connect/entrypoints.d.ts.map +1 -1
- package/build/connect/entrypoints.js +292 -9
- package/build/connect/entrypoints.js.map +1 -1
- package/build/connect/errors.d.ts +12 -0
- package/build/connect/errors.d.ts.map +1 -1
- package/build/connect/errors.js +16 -0
- package/build/connect/errors.js.map +1 -1
- package/build/connect/feed/types.d.ts +71 -0
- package/build/connect/feed/types.d.ts.map +1 -0
- package/build/connect/feed/types.js +2 -0
- package/build/connect/feed/types.js.map +1 -0
- package/build/connect/files/types.d.ts +34 -0
- package/build/connect/files/types.d.ts.map +1 -0
- package/build/connect/files/types.js +2 -0
- package/build/connect/files/types.js.map +1 -0
- package/build/connect/git/types.d.ts +170 -0
- package/build/connect/git/types.d.ts.map +1 -0
- package/build/connect/git/types.js +14 -0
- package/build/connect/git/types.js.map +1 -0
- package/build/connect/iam/types.d.ts +25 -0
- package/build/connect/iam/types.d.ts.map +1 -0
- package/build/connect/iam/types.js +8 -0
- package/build/connect/iam/types.js.map +1 -0
- package/build/connect/index.d.ts +9 -0
- package/build/connect/index.d.ts.map +1 -1
- package/build/connect/ops.d.ts +189 -0
- package/build/connect/ops.d.ts.map +1 -0
- package/build/connect/ops.js +2 -0
- package/build/connect/ops.js.map +1 -0
- package/build/connect/production/types.d.ts +65 -0
- package/build/connect/production/types.d.ts.map +1 -0
- package/build/connect/production/types.js +2 -0
- package/build/connect/production/types.js.map +1 -0
- package/build/connect/project/types.d.ts +23 -16
- package/build/connect/project/types.d.ts.map +1 -1
- package/build/connect/references/types.d.ts +424 -2
- package/build/connect/references/types.d.ts.map +1 -1
- package/build/connect/references.d.ts.map +1 -1
- package/build/connect/references.js +122 -0
- package/build/connect/references.js.map +1 -1
- package/build/connect/schemas.d.ts +65 -2
- package/build/connect/schemas.d.ts.map +1 -1
- package/build/connect/schemas.js +306 -1
- package/build/connect/schemas.js.map +1 -1
- package/build/connect/settings/types.d.ts +0 -25
- package/build/connect/settings/types.d.ts.map +1 -1
- package/build/connect/slot/types.d.ts +24 -0
- package/build/connect/slot/types.d.ts.map +1 -0
- package/build/connect/slot/types.js +2 -0
- package/build/connect/slot/types.js.map +1 -0
- package/build/connect/types.d.ts +453 -0
- package/build/connect/types.d.ts.map +1 -0
- package/build/connect/types.js +2 -0
- package/build/connect/types.js.map +1 -0
- package/build/convert/record/types.d.ts +0 -9
- package/build/convert/record/types.d.ts.map +1 -1
- package/build/convert/schemas.d.ts +2 -5
- package/build/convert/schemas.d.ts.map +1 -1
- package/build/convert/schemas.js +2 -15
- package/build/convert/schemas.js.map +1 -1
- package/build/iam-console/consts.d.ts +13 -0
- package/build/iam-console/consts.d.ts.map +1 -0
- package/build/iam-console/consts.js +13 -0
- package/build/iam-console/consts.js.map +1 -0
- package/build/iam-console/consts.local.d.ts +123 -0
- package/build/iam-console/consts.local.d.ts.map +1 -0
- package/build/iam-console/consts.local.js +50 -0
- package/build/iam-console/consts.local.js.map +1 -0
- package/build/iam-console/index.d.ts +4 -0
- package/build/iam-console/index.d.ts.map +1 -0
- package/build/iam-console/index.js +3 -0
- package/build/iam-console/index.js.map +1 -0
- package/build/iam-console/schemas.d.ts +38 -0
- package/build/iam-console/schemas.d.ts.map +1 -0
- package/build/iam-console/schemas.js +217 -0
- package/build/iam-console/schemas.js.map +1 -0
- package/build/iam-console/types.d.ts +140 -0
- package/build/iam-console/types.d.ts.map +1 -0
- package/build/iam-console/types.js +2 -0
- package/build/iam-console/types.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/intent/flow.d.ts +17 -0
- package/build/intent/flow.d.ts.map +1 -0
- package/build/intent/flow.js +65 -0
- package/build/intent/flow.js.map +1 -0
- package/build/legal/consts.d.ts +67 -0
- package/build/legal/consts.d.ts.map +1 -0
- package/build/legal/consts.js +24 -0
- package/build/legal/consts.js.map +1 -0
- package/build/legal/index.d.ts +3 -0
- package/build/legal/index.d.ts.map +1 -0
- package/build/legal/index.js +2 -0
- package/build/legal/index.js.map +1 -0
- package/build/legal/types.d.ts +17 -0
- package/build/legal/types.d.ts.map +1 -0
- package/build/legal/types.js +2 -0
- package/build/legal/types.js.map +1 -0
- package/build/planning/flows.d.ts +34 -0
- package/build/planning/flows.d.ts.map +1 -0
- package/build/planning/flows.js +174 -0
- package/build/planning/flows.js.map +1 -0
- package/build/planning/schemas.d.ts.map +1 -1
- package/build/planning/schemas.js +0 -1
- package/build/planning/schemas.js.map +1 -1
- package/build/planning/types.d.ts +0 -2
- package/build/planning/types.d.ts.map +1 -1
- package/build/skills/roles.d.ts +43 -0
- package/build/skills/roles.d.ts.map +1 -0
- package/build/skills/roles.js +44 -0
- package/build/skills/roles.js.map +1 -0
- package/package.json +19 -3
- package/src/connect/account/types.ts +91 -0
- package/src/connect/branding/types.ts +28 -4
- package/src/connect/call/types.ts +31 -0
- package/src/connect/config/types.ts +51 -0
- package/src/connect/consts.ts +400 -1
- package/src/connect/conversion/types.ts +25 -0
- package/src/connect/entrypoints/types.ts +22 -7
- package/src/connect/entrypoints.ts +592 -15
- package/src/connect/errors.ts +18 -0
- package/src/connect/feed/types.ts +76 -0
- package/src/connect/files/types.ts +38 -0
- package/src/connect/git/types.ts +187 -0
- package/src/connect/iam/types.ts +28 -0
- package/src/connect/index.ts +9 -0
- package/src/connect/production/types.ts +71 -0
- package/src/connect/project/types.ts +25 -12
- package/src/connect/references/types.ts +166 -5
- package/src/connect/references.ts +210 -5
- package/src/connect/schemas.ts +366 -5
- package/src/connect/settings/types.ts +0 -25
- package/src/connect/slot/types.ts +24 -0
- package/src/convert/record/types.ts +0 -11
- package/src/convert/schemas.ts +3 -19
- package/src/iam-console/consts.local.ts +64 -0
- package/src/iam-console/consts.ts +15 -0
- package/src/iam-console/index.ts +3 -0
- package/src/iam-console/schemas.ts +251 -0
- package/src/iam-console/types.ts +165 -0
- package/src/index.ts +2 -0
- package/src/legal/consts.ts +25 -0
- package/src/legal/index.ts +2 -0
- package/src/legal/types.ts +22 -0
- package/src/planning/schemas.ts +0 -1
- package/src/planning/types.ts +0 -2
- package/tests/connect-convert.spec.ts +6 -2
- package/tests/connect-entrypoints.spec.ts +554 -16
- package/tests/error-status.spec.ts +2 -1
- package/tests/iam-console.spec.ts +89 -0
- package/tests/planning.spec.ts +8 -0
|
@@ -3,10 +3,21 @@ import Ajv from 'ajv'
|
|
|
3
3
|
import { aliasOf, protocols } from '@owlmeans/entrypoint'
|
|
4
4
|
import { DEFAULT_GUARD } from '@owlmeans/auth-common'
|
|
5
5
|
import { RouteMethod, RouteProtocols } from '@owlmeans/route'
|
|
6
|
-
import { connect, CONNECT_BRANDING_GOOGLE_TAG_MAX } from '../src/connect/consts.js'
|
|
7
6
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
connect, CONNECT_BRANDING_GOOGLE_TAG_MAX, CONNECT_CALL_ACCEPT_MS, CONNECT_CALL_COLLECT_WAIT_SEC,
|
|
8
|
+
CONNECT_CALL_HEADER, CONNECT_CALL_LOST_MS, CONNECT_CONFIG_SAVE_MAX, CONNECT_FILE_PATH_MAX, CONNECT_PRIVACY_KEYS_MAX,
|
|
9
|
+
CONNECT_REDIRECTS_MAX, CONNECT_TOKEN_ID_MAX, ConnectCallState, ConnectPaidGate,
|
|
10
|
+
CONNECT_FEED_LIMIT_MAX, CONNECT_FEED_START, CONNECT_FEED_WAIT_MAX_SEC, ConnectFeedDetail, ConnectFeedKind,
|
|
11
|
+
} from '../src/connect/consts.js'
|
|
12
|
+
import {
|
|
13
|
+
ConnectAccessTokenParamsSchema, ConnectIntentPickupBodySchema, ConnectLlmBodySchema, ConnectPrivacyWithdrawBodySchema,
|
|
14
|
+
ConnectProjectLlmBodySchema, ConnectBrandingCreditBodySchema, ConnectCallCollectParamsSchema, ConnectCallCollectQuerySchema, ConnectConfigSaveBodySchema,
|
|
15
|
+
ConnectOrganizationBrandingSaveSchema, ConnectScopeQuerySchema, ConnectCallPendingSchema, ConnectCallResultSchema,
|
|
16
|
+
ConnectConvertProceedBodySchema, ConnectCreateBodySchema, ConnectFileMetaQuerySchema, ConnectFileQuerySchema,
|
|
17
|
+
ConnectFileSaveBodySchema, ConnectGitCommitBodySchema, ConnectGitRevertBodySchema, ConnectGithubBranchQuerySchema,
|
|
18
|
+
ConnectGithubLinkBodySchema, ConnectGithubPublishBodySchema, ConnectGithubRepoQuerySchema, ConnectKitApplyBodySchema,
|
|
19
|
+
ConnectProductionDomainBodySchema, ConnectProductionRedirectsBodySchema, ConnectKitApplyResultSchema, ConnectKitDescribeSchema, ConnectProjectBrandingSaveSchema,
|
|
20
|
+
PlanningKitViewSchema, ConnectActivityQuerySchema, ConnectFeedQuerySchema,
|
|
10
21
|
} from '../src/connect/schemas.js'
|
|
11
22
|
import { connectProtocols } from '../src/connect/entrypoints.js'
|
|
12
23
|
import { connectRef } from '../src/connect/references.js'
|
|
@@ -21,8 +32,27 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
21
32
|
expect(tree.convert.proceed.alias).toBe(connect.convert.proceed)
|
|
22
33
|
expect(tree.inquiry.answer.alias).toBe(connect.inquiry.answer)
|
|
23
34
|
expect(tree.files.list.alias).toBe(connect.files.list)
|
|
24
|
-
expect(
|
|
25
|
-
expect(
|
|
35
|
+
expect(tree.account.base.alias).toBe(connect.account.base)
|
|
36
|
+
expect(tree.project.destroy.alias).toBe(connect.project.destroy)
|
|
37
|
+
expect(tree.project.unlock.alias).toBe(connect.project.unlock)
|
|
38
|
+
expect(tree.files.save.alias).toBe(connect.files.save)
|
|
39
|
+
expect(tree.sandbox.run.alias).toBe(connect.sandbox.run)
|
|
40
|
+
expect(tree.slot.list.alias).toBe(connect.slot.list)
|
|
41
|
+
expect(tree.config.get.alias).toBe(connect.config.get)
|
|
42
|
+
expect(tree.account.branding.save.alias).toBe(connect.account.branding.save)
|
|
43
|
+
expect(tree.account.llm.set.alias).toBe(connect.account.llm.set)
|
|
44
|
+
expect(tree.account.tokens.revoke.alias).toBe(connect.account.tokens.revoke)
|
|
45
|
+
expect(tree.account.privacy.withdraw.alias).toBe(connect.account.privacy.withdraw)
|
|
46
|
+
expect(tree.account.intent.pickup.alias).toBe(connect.account.intent.pickup)
|
|
47
|
+
expect(tree.project.llm.set.alias).toBe(connect.project.llm.set)
|
|
48
|
+
expect(tree.git.status.alias).toBe(connect.git.status)
|
|
49
|
+
expect(tree.github.authorize.alias).toBe(connect.github.authorize)
|
|
50
|
+
expect(tree.production.publish.alias).toBe(connect.production.publish)
|
|
51
|
+
expect(tree.production.auth.get.alias).toBe(connect.production.auth.get)
|
|
52
|
+
expect(tree.iam.grants.assign.alias).toBe(connect.iam.grants.assign)
|
|
53
|
+
expect(tree.account.iam.users.alias).toBe(connect.account.iam.users)
|
|
54
|
+
expect(protocols(tree)).toHaveLength(107)
|
|
55
|
+
expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(107)
|
|
26
56
|
})
|
|
27
57
|
|
|
28
58
|
test('declares exactly the routes a connector calls — every alias, reference and path, and no socket', () => {
|
|
@@ -31,21 +61,56 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
31
61
|
.map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`).sort()
|
|
32
62
|
|
|
33
63
|
expect(addresses).toEqual([
|
|
34
|
-
'
|
|
35
|
-
'GET /
|
|
36
|
-
'GET /
|
|
64
|
+
'DELETE /project/:id', 'DELETE /project/:id/files/content',
|
|
65
|
+
'GET /access-tokens', 'GET /branding', 'GET /call/:callId', 'GET /convert/:id', 'GET /convert/:id/check',
|
|
66
|
+
'GET /iam/users', 'GET /llm', 'GET /notifications', 'GET /pipeline/:id/:runId',
|
|
67
|
+
'GET /privacy', 'GET /project', 'GET /project/:id/activity',
|
|
68
|
+
'GET /project/:id/branding', 'GET /project/:id/config', 'GET /project/:id/files', 'GET /project/:id/files/changes',
|
|
69
|
+
'GET /project/:id/files/content',
|
|
70
|
+
'GET /project/:id/files/meta', 'GET /project/:id/git', 'GET /project/:id/git/log', 'GET /project/:id/github/branches',
|
|
71
|
+
'GET /project/:id/github/repos', 'GET /project/:id/iam/grants', 'GET /project/:id/iam/organizations',
|
|
72
|
+
'GET /project/:id/iam/organizations/:entitySlug/groups', 'GET /project/:id/iam/organizations/:entitySlug/groups/:group/members',
|
|
73
|
+
'GET /project/:id/iam/organizations/:entitySlug/members', 'GET /project/:id/iam/permissions', 'GET /project/:id/iam/users',
|
|
74
|
+
'GET /project/:id/kits', 'GET /project/:id/llm', 'GET /project/:id/production',
|
|
75
|
+
'GET /project/:id/production/auth', 'GET /project/:id/status',
|
|
76
|
+
'GET /project/:id/story/:storyId/status', 'GET /session/:sessionId/ops', 'GET /slot',
|
|
77
|
+
'POST /access-tokens/:id/revoke', 'POST /branding', 'POST /branding/backfill',
|
|
37
78
|
'POST /convert', 'POST /convert/:id/proceed', 'POST /convert/:id/purge', 'POST /convert/:id/start',
|
|
38
|
-
'POST /
|
|
39
|
-
'POST /
|
|
40
|
-
'POST /project/:id/
|
|
41
|
-
'POST /
|
|
79
|
+
'POST /intent/pickup', 'POST /llm',
|
|
80
|
+
'POST /pipeline/:id/:runId/resume', 'POST /privacy/withdraw', 'POST /project', 'POST /project/:id/branding',
|
|
81
|
+
'POST /project/:id/branding/copy-defaults', 'POST /project/:id/branding/credit',
|
|
82
|
+
'POST /project/:id/config', 'POST /project/:id/config/recollect', 'POST /project/:id/confirm', 'POST /project/:id/files/content',
|
|
83
|
+
'POST /project/:id/git/commit', 'POST /project/:id/git/discard', 'POST /project/:id/git/revert',
|
|
84
|
+
'POST /project/:id/github/authorize', 'POST /project/:id/github/disconnect', 'POST /project/:id/github/link',
|
|
85
|
+
'POST /project/:id/github/publish', 'POST /project/:id/github/pull', 'POST /project/:id/github/push',
|
|
86
|
+
'POST /project/:id/iam/grants', 'POST /project/:id/iam/grants/revoke',
|
|
87
|
+
'POST /project/:id/iam/organizations/:entitySlug', 'POST /project/:id/iam/organizations/:entitySlug/groups',
|
|
88
|
+
'POST /project/:id/iam/organizations/:entitySlug/groups/:group',
|
|
89
|
+
'POST /project/:id/iam/organizations/:entitySlug/groups/:group/members',
|
|
90
|
+
'POST /project/:id/iam/organizations/:entitySlug/groups/:group/members/remove',
|
|
91
|
+
'POST /project/:id/iam/organizations/:entitySlug/groups/:group/remove',
|
|
92
|
+
'POST /project/:id/iam/organizations/:entitySlug/members',
|
|
93
|
+
'POST /project/:id/iam/organizations/:entitySlug/members/:profileId',
|
|
94
|
+
'POST /project/:id/iam/organizations/:entitySlug/members/:profileId/remove',
|
|
95
|
+
'POST /project/:id/iam/permissions/default', 'POST /project/:id/iam/users', 'POST /project/:id/iam/users/:profileId',
|
|
96
|
+
'POST /project/:id/iam/users/:profileId/remove',
|
|
97
|
+
'POST /project/:id/inquiry/:inquiryId',
|
|
98
|
+
'POST /project/:id/kits', 'POST /project/:id/llm', 'POST /project/:id/modify',
|
|
99
|
+
'POST /project/:id/production/auth/redirects', 'POST /project/:id/production/domain/attach',
|
|
100
|
+
'POST /project/:id/production/domain/detach', 'POST /project/:id/production/domain/verify',
|
|
101
|
+
'POST /project/:id/production/publish', 'POST /project/:id/production/restart', 'POST /project/:id/production/stop',
|
|
102
|
+
'POST /project/:id/reinit', 'POST /project/:id/rename',
|
|
103
|
+
'POST /project/:id/sandbox/rebuild', 'POST /project/:id/sandbox/restart', 'POST /project/:id/sandbox/run',
|
|
104
|
+
'POST /project/:id/sandbox/stop', 'POST /project/:id/unlock', 'POST /project/attach', 'POST /session',
|
|
105
|
+
'POST /session/:sessionId/close', 'POST /session/:sessionId/ops/:opId', 'POST /session/delegated',
|
|
42
106
|
])
|
|
43
107
|
expect(declared.every(protocol => protocol.route.route.protocol !== RouteProtocols.SOCKET)).toBe(true)
|
|
44
108
|
// The alias table, the tree and the typed references name the same routes.
|
|
45
109
|
const leaves = (value: object): string[] => Object.values(value).flatMap(entry =>
|
|
46
110
|
typeof entry === 'string' ? [entry] : 'alias' in entry ? [entry.alias as string] : leaves(entry))
|
|
47
111
|
expect(leaves(connect).sort()).toEqual(declared.map(protocol => protocol.alias).sort())
|
|
48
|
-
|
|
112
|
+
const bases = [connect.base, connect.account.base]
|
|
113
|
+
expect(leaves(connectRef).sort()).toEqual(declared.map(protocol => protocol.alias).filter(alias => !bases.includes(alias)).sort())
|
|
49
114
|
})
|
|
50
115
|
|
|
51
116
|
test('reads and saves a project\'s branding at one path, under the owned base and no paid gate', () => {
|
|
@@ -65,17 +130,92 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
65
130
|
expect(save.route.route.method).toBe(RouteMethod.POST)
|
|
66
131
|
expect(aliasOf(get.route.route.parent)).toBe(connect.base)
|
|
67
132
|
expect(aliasOf(save.route.route.parent)).toBe(connect.base)
|
|
68
|
-
// The paid local-LLM gate sits on the delegated session
|
|
69
|
-
// own and
|
|
133
|
+
// The paid local-LLM gate sits on the delegated session and the two preference writes alone; the
|
|
134
|
+
// text settings carry no gate of their own and inherit only the base's guard and ownership gate.
|
|
70
135
|
expect(paid.session.openDelegated.gate?.alias).toBe('test-paid-llm')
|
|
71
136
|
expect(protocols(paid).filter(protocol => protocol.gate?.alias === 'test-paid-llm'))
|
|
72
|
-
.toEqual([paid.session.openDelegated])
|
|
137
|
+
.toEqual([paid.account.llm.set, paid.session.openDelegated, paid.project.llm.set])
|
|
73
138
|
expect(get.gate).toBeUndefined()
|
|
74
139
|
expect(save.gate).toBeUndefined()
|
|
75
140
|
expect(connectRef.project.branding.save.alias).toBe(connect.project.branding.save)
|
|
76
141
|
expect(connectRef.project.branding.get.alias).toBe(connect.project.branding.get)
|
|
77
142
|
})
|
|
78
143
|
|
|
144
|
+
test('the credit switch and the organization defaults copy hang under the project\'s branding path', () => {
|
|
145
|
+
const { credit, copyDefaults } = tree.project.branding
|
|
146
|
+
|
|
147
|
+
expect([credit.route.route.path, copyDefaults.route.route.path])
|
|
148
|
+
.toEqual(['/project/:id/branding/credit', '/project/:id/branding/copy-defaults'])
|
|
149
|
+
expect([credit.route.route.method, copyDefaults.route.route.method]).toEqual([RouteMethod.POST, RouteMethod.POST])
|
|
150
|
+
expect([aliasOf(credit.route.route.parent), aliasOf(copyDefaults.route.route.parent)]).toEqual([connect.base, connect.base])
|
|
151
|
+
expect([connectRef.project.branding.credit.alias, connectRef.project.branding.copyDefaults.alias])
|
|
152
|
+
.toEqual([connect.project.branding.credit, connect.project.branding.copyDefaults])
|
|
153
|
+
const body = new Ajv({ strict: false }).compile(ConnectBrandingCreditBodySchema)
|
|
154
|
+
expect(body({ hideCredit: true })).toBe(true)
|
|
155
|
+
expect(body({})).toBe(false)
|
|
156
|
+
expect(body({ hideCredit: 'yes' })).toBe(false)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('the configuration routes sit under the owned base, no paid gate; a scope is preview or production', () => {
|
|
160
|
+
const paid = connectProtocols({
|
|
161
|
+
guard: DEFAULT_GUARD,
|
|
162
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
163
|
+
paid: Object.fromEntries(Object.values(ConnectPaidGate).map(kind => [kind, { alias: `gate-${kind}`, params: [kind] }])),
|
|
164
|
+
})
|
|
165
|
+
const { get, save, recollect } = paid.config
|
|
166
|
+
|
|
167
|
+
expect([get, save, recollect].map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`))
|
|
168
|
+
.toEqual(['GET /project/:id/config', 'POST /project/:id/config', 'POST /project/:id/config/recollect'])
|
|
169
|
+
expect([get, save, recollect].map(protocol => aliasOf(protocol.route.route.parent))).toEqual([connect.base, connect.base, connect.base])
|
|
170
|
+
expect([get, save, recollect].map(protocol => protocol.gate)).toEqual([undefined, undefined, undefined])
|
|
171
|
+
expect(connectRef.config.save.alias).toBe(connect.config.save)
|
|
172
|
+
|
|
173
|
+
const scope = new Ajv({ strict: false }).compile(ConnectScopeQuerySchema)
|
|
174
|
+
expect(scope({})).toBe(true)
|
|
175
|
+
expect(scope({ scope: 'production' })).toBe(true)
|
|
176
|
+
expect(scope({ scope: 'ephemeral' })).toBe(true)
|
|
177
|
+
expect(scope({ scope: null })).toBe(true)
|
|
178
|
+
expect(scope({ scope: 'local' })).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('a configuration save is a closed patch of environment variable names and values', () => {
|
|
182
|
+
const validate = new Ajv({ strict: false }).compile(ConnectConfigSaveBodySchema)
|
|
183
|
+
|
|
184
|
+
expect(validate({})).toBe(true)
|
|
185
|
+
expect(validate({ backend: [{ name: 'STRIPE_KEY', value: 'sk_test' }], frontend: [{ name: 'PUBLIC_URL', value: '' }] })).toBe(true)
|
|
186
|
+
expect(validate({ backend: [{ name: '1BAD', value: 'x' }] })).toBe(false)
|
|
187
|
+
expect(validate({ backend: [{ name: 'WITH-DASH', value: 'x' }] })).toBe(false)
|
|
188
|
+
expect(validate({ backend: [{ name: 'KEY' }] })).toBe(false)
|
|
189
|
+
expect(validate({ backend: [{ name: 'KEY', value: 'x', set: true }] })).toBe(false)
|
|
190
|
+
expect(validate({ secrets: [] })).toBe(false)
|
|
191
|
+
const many = Array.from({ length: CONNECT_CONFIG_SAVE_MAX + 1 }, (_, at) => ({ name: `K${at}`, value: '' }))
|
|
192
|
+
expect(validate({ frontend: many })).toBe(false)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('the organization defaults are read, patched and backfilled under the account base', () => {
|
|
196
|
+
const { get, save, backfill } = tree.account.branding
|
|
197
|
+
|
|
198
|
+
expect([get, save, backfill].map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`))
|
|
199
|
+
.toEqual(['GET /branding', 'POST /branding', 'POST /branding/backfill'])
|
|
200
|
+
expect([get, save, backfill].map(protocol => aliasOf(protocol.route.route.parent)))
|
|
201
|
+
.toEqual([connect.account.base, connect.account.base, connect.account.base])
|
|
202
|
+
expect(connectRef.account.branding.backfill.alias).toBe(connect.account.branding.backfill)
|
|
203
|
+
const validate = new Ajv({ strict: false }).compile(ConnectOrganizationBrandingSaveSchema)
|
|
204
|
+
expect(validate({ organizationName: 'Acme' })).toBe(true)
|
|
205
|
+
expect(validate({ organizationName: '' })).toBe(false)
|
|
206
|
+
expect(validate({ copyright: '© Acme', termsUrl: '/terms' })).toBe(false)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('a create names the caller\'s unattached delegated session, or none', () => {
|
|
210
|
+
const create = new Ajv({ strict: false }).compile(ConnectCreateBodySchema)
|
|
211
|
+
|
|
212
|
+
expect(create({ prompt: 'A booking app' })).toBe(true)
|
|
213
|
+
expect(create({ prompt: 'A booking app', target: 'local', sessionId: 'session-1' })).toBe(true)
|
|
214
|
+
expect(create({ prompt: 'A booking app', sessionId: null })).toBe(true)
|
|
215
|
+
expect(create({ prompt: 'A booking app', sessionId: '' })).toBe(false)
|
|
216
|
+
expect(create({ prompt: 'A booking app', llmMode: 'local' })).toBe(false)
|
|
217
|
+
})
|
|
218
|
+
|
|
79
219
|
test('the branding save body is a patch of strings that can never carry the credit', () => {
|
|
80
220
|
const validate = new Ajv({ strict: false }).compile(ConnectProjectBrandingSaveSchema)
|
|
81
221
|
|
|
@@ -83,6 +223,7 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
83
223
|
expect(validate({ googleTag: 'GTM-ABCD123', termsUrl: '/terms', privacyUrl: '' })).toBe(true)
|
|
84
224
|
expect(validate({ copyright: '' })).toBe(false)
|
|
85
225
|
expect(validate({ hideCredit: true })).toBe(false)
|
|
226
|
+
expect(validate({ credit: { hidden: true } })).toBe(false)
|
|
86
227
|
expect(validate({ googleTag: 'G-'.padEnd(CONNECT_BRANDING_GOOGLE_TAG_MAX + 1, 'X') })).toBe(false)
|
|
87
228
|
})
|
|
88
229
|
|
|
@@ -141,4 +282,401 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
141
282
|
expect(result({ applied: ['task'], skipped: ['bug'], warnings: [] })).toBe(true)
|
|
142
283
|
expect(result({ applied: ['task'], skipped: [] })).toBe(false)
|
|
143
284
|
})
|
|
285
|
+
|
|
286
|
+
test('collects a delegated write by its call id: one GET under the base guard and no paid gate', () => {
|
|
287
|
+
const paid = connectProtocols({
|
|
288
|
+
guard: DEFAULT_GUARD,
|
|
289
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
290
|
+
localLlm: { alias: 'test-paid-llm', params: ['cap'] },
|
|
291
|
+
})
|
|
292
|
+
const collect = paid.call.collect
|
|
293
|
+
|
|
294
|
+
expect(collect.alias).toBe(connect.call.collect)
|
|
295
|
+
expect(collect.route.route.path).toBe('/call/:callId')
|
|
296
|
+
expect(collect.route.route.method).toBe(RouteMethod.GET)
|
|
297
|
+
expect(aliasOf(collect.route.route.parent)).toBe(connect.base)
|
|
298
|
+
expect(collect.gate).toBeUndefined()
|
|
299
|
+
expect(connectRef.call.collect.alias).toBe(connect.call.collect)
|
|
300
|
+
// The timing contract the platform and the connector both read.
|
|
301
|
+
expect(CONNECT_CALL_HEADER).toBe('x-viable-call')
|
|
302
|
+
expect([CONNECT_CALL_ACCEPT_MS, CONNECT_CALL_COLLECT_WAIT_SEC, CONNECT_CALL_LOST_MS]).toEqual([20_000, 25, 90_000])
|
|
303
|
+
expect(Object.values(ConnectCallState)).toEqual(['pending', 'settled', 'lost'])
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('the pending answer, the collect params and query, and the result are closed shapes', () => {
|
|
307
|
+
const ajv = new Ajv({ strict: false })
|
|
308
|
+
const pending = ajv.compile(ConnectCallPendingSchema)
|
|
309
|
+
const params = ajv.compile(ConnectCallCollectParamsSchema)
|
|
310
|
+
const query = ajv.compile(ConnectCallCollectQuerySchema)
|
|
311
|
+
const result = ajv.compile(ConnectCallResultSchema)
|
|
312
|
+
const id = '0b6f6c1e-8d4c-4a57-9f2a-3c1d2e4f5a6b'
|
|
313
|
+
|
|
314
|
+
expect(pending({ pending: id })).toBe(true)
|
|
315
|
+
expect(pending({ pending: '' })).toBe(false)
|
|
316
|
+
expect(pending({ pending: id, value: 1 })).toBe(false)
|
|
317
|
+
expect(params({ callId: id })).toBe(true)
|
|
318
|
+
expect(params({})).toBe(false)
|
|
319
|
+
expect(query({})).toBe(true)
|
|
320
|
+
expect(query({ wait: CONNECT_CALL_COLLECT_WAIT_SEC })).toBe(true)
|
|
321
|
+
expect(query({ wait: null })).toBe(true)
|
|
322
|
+
expect(query({ wait: CONNECT_CALL_COLLECT_WAIT_SEC + 1 })).toBe(false)
|
|
323
|
+
expect(result({ state: 'pending' })).toBe(true)
|
|
324
|
+
// Any JSON as the value — an object, an array, a scalar, null.
|
|
325
|
+
for (const value of [{ project: { id: 'p1' } }, ['a'], 'text', 3, null]) {
|
|
326
|
+
expect(result({ state: 'settled', outcome: 'ok', value })).toBe(true)
|
|
327
|
+
}
|
|
328
|
+
expect(result({ state: 'settled', error: 'ViableConnectResilientError|||viable-connect:x|||' })).toBe(true)
|
|
329
|
+
expect(result({ state: 'lost' })).toBe(true)
|
|
330
|
+
expect(result({ state: 'done' })).toBe(false)
|
|
331
|
+
expect(result({ state: 'settled', extra: true })).toBe(false)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('the account base is a root of its own, under the account gate, never a child of the project base', () => {
|
|
335
|
+
const gated = connectProtocols({
|
|
336
|
+
guard: DEFAULT_GUARD,
|
|
337
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
338
|
+
accountGate: { alias: 'test-gate', params: ['account'] },
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
expect(gated.account.base.alias).toBe(connect.account.base)
|
|
342
|
+
expect(gated.account.base.route.route.path).toBe('/connect/account')
|
|
343
|
+
expect(gated.account.base.route.route.parent).toBeUndefined()
|
|
344
|
+
expect(gated.account.base.route.route.method).toBeUndefined()
|
|
345
|
+
expect(gated.account.base.gate).toEqual({ alias: 'test-gate', params: ['account'] })
|
|
346
|
+
expect(gated.account.base.guards).toEqual([DEFAULT_GUARD])
|
|
347
|
+
expect(gated.base.gate).toEqual({ alias: 'test-gate', params: ['owner'] })
|
|
348
|
+
// Without an account gate the base still carries the guard.
|
|
349
|
+
expect(tree.account.base.gate).toBeUndefined()
|
|
350
|
+
expect(tree.account.base.guards).toEqual([DEFAULT_GUARD])
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test('the paid map injects each gate by kind, and the deprecated local-LLM option still reads', () => {
|
|
354
|
+
const paid = Object.fromEntries(Object.values(ConnectPaidGate)
|
|
355
|
+
.map(kind => [kind, { alias: `gate-${kind}`, params: [`param:${kind}`] }]))
|
|
356
|
+
const mapped = connectProtocols({ guard: DEFAULT_GUARD, paid })
|
|
357
|
+
|
|
358
|
+
expect(Object.values(ConnectPaidGate)).toEqual([
|
|
359
|
+
'local-llm', 'whitelabel', 'custom-domain', 'production-standalone', 'published-sites',
|
|
360
|
+
])
|
|
361
|
+
expect(mapped.session.openDelegated.gate).toEqual({ alias: 'gate-local-llm', params: ['param:local-llm'] })
|
|
362
|
+
// Only the routes that spend something carry a payment gate.
|
|
363
|
+
expect(protocols(mapped).filter(protocol => protocol.gate?.alias.startsWith('gate-')).map(protocol => protocol.alias))
|
|
364
|
+
.toEqual([
|
|
365
|
+
connect.account.llm.set, connect.session.openDelegated, connect.project.llm.set, connect.project.branding.credit,
|
|
366
|
+
connect.production.publish, connect.production.domain.attach, connect.production.auth.get,
|
|
367
|
+
connect.production.auth.redirects,
|
|
368
|
+
])
|
|
369
|
+
expect([mapped.account.llm.set.gate, mapped.project.llm.set.gate]).toEqual([
|
|
370
|
+
{ alias: 'gate-local-llm', params: ['param:local-llm'] }, { alias: 'gate-local-llm', params: ['param:local-llm'] },
|
|
371
|
+
])
|
|
372
|
+
expect(mapped.project.branding.credit.gate).toEqual({ alias: 'gate-whitelabel', params: ['param:whitelabel'] })
|
|
373
|
+
expect([
|
|
374
|
+
mapped.production.publish.gate, mapped.production.domain.attach.gate, mapped.production.auth.get.gate,
|
|
375
|
+
mapped.production.auth.redirects.gate,
|
|
376
|
+
]).toEqual([
|
|
377
|
+
{ alias: 'gate-published-sites', params: ['param:published-sites'] },
|
|
378
|
+
{ alias: 'gate-custom-domain', params: ['param:custom-domain'] },
|
|
379
|
+
{ alias: 'gate-production-standalone', params: ['param:production-standalone'] },
|
|
380
|
+
{ alias: 'gate-production-standalone', params: ['param:production-standalone'] },
|
|
381
|
+
])
|
|
382
|
+
// A deployment that names no white-label gate leaves the credit switch under ownership alone.
|
|
383
|
+
expect(connectProtocols({ guard: DEFAULT_GUARD }).project.branding.credit.gate).toBeUndefined()
|
|
384
|
+
|
|
385
|
+
const legacy = connectProtocols({ guard: DEFAULT_GUARD, localLlm: { alias: 'legacy', params: ['cap'] } })
|
|
386
|
+
expect(legacy.session.openDelegated.gate).toEqual({ alias: 'legacy', params: ['cap'] })
|
|
387
|
+
// The map wins over the deprecated option.
|
|
388
|
+
const both = connectProtocols({
|
|
389
|
+
guard: DEFAULT_GUARD, localLlm: { alias: 'legacy', params: ['cap'] },
|
|
390
|
+
paid: { [ConnectPaidGate.LocalLlm]: { alias: 'mapped', params: ['cap'] } },
|
|
391
|
+
})
|
|
392
|
+
expect(both.session.openDelegated.gate?.alias).toBe('mapped')
|
|
393
|
+
expect(connectProtocols({ guard: DEFAULT_GUARD }).session.openDelegated.gate).toBeUndefined()
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
test('deletes and unlocks a project under the owned base, no payment gate', () => {
|
|
397
|
+
const paid = connectProtocols({
|
|
398
|
+
guard: DEFAULT_GUARD,
|
|
399
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
400
|
+
localLlm: { alias: 'test-paid-llm', params: ['cap'] },
|
|
401
|
+
})
|
|
402
|
+
const { destroy, unlock } = paid.project
|
|
403
|
+
|
|
404
|
+
expect([destroy.route.route.path, unlock.route.route.path]).toEqual(['/project/:id', '/project/:id/unlock'])
|
|
405
|
+
expect([destroy.route.route.method, unlock.route.route.method]).toEqual([RouteMethod.DELETE, RouteMethod.POST])
|
|
406
|
+
expect([aliasOf(destroy.route.route.parent), aliasOf(unlock.route.route.parent)]).toEqual([connect.base, connect.base])
|
|
407
|
+
expect([destroy.gate, unlock.gate]).toEqual([undefined, undefined])
|
|
408
|
+
expect([connectRef.project.destroy.alias, connectRef.project.unlock.alias])
|
|
409
|
+
.toEqual([connect.project.destroy, connect.project.unlock])
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('a proceed carries the person\'s edits as an optional closed patch', () => {
|
|
413
|
+
const validate = new Ajv({ strict: false }).compile(ConnectConvertProceedBodySchema)
|
|
414
|
+
|
|
415
|
+
expect(validate({ decision: 'extract' })).toBe(true)
|
|
416
|
+
expect(validate({ decision: 'extract', update: { name: 'Shop', specification: '# Spec' } })).toBe(true)
|
|
417
|
+
expect(validate({ decision: 'extract', update: {} })).toBe(true)
|
|
418
|
+
expect(validate({ decision: 'extract', update: null })).toBe(true)
|
|
419
|
+
expect(validate({ decision: 'extract', update: { name: '' } })).toBe(false)
|
|
420
|
+
// The alias is not part of the patch: the preview host and the slot were composed from it.
|
|
421
|
+
expect(validate({ decision: 'extract', update: { alias: 'other' } })).toBe(false)
|
|
422
|
+
expect(validate({ decision: 'extract', update: { title: 'Shop' } })).toBe(false)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
test('reads, writes and deletes one file at one path, lists metadata, and controls the preview — owned base, no paid gate', () => {
|
|
426
|
+
const paid = connectProtocols({
|
|
427
|
+
guard: DEFAULT_GUARD,
|
|
428
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
429
|
+
paid: Object.fromEntries(Object.values(ConnectPaidGate).map(kind => [kind, { alias: `gate-${kind}`, params: [kind] }])),
|
|
430
|
+
})
|
|
431
|
+
const { get, save, remove, meta } = paid.files
|
|
432
|
+
const { run, restart, stop, rebuild } = paid.sandbox
|
|
433
|
+
const leaves = [get, save, remove, meta, run, restart, stop, rebuild, paid.slot.list]
|
|
434
|
+
|
|
435
|
+
expect([get, save, remove].map(protocol => protocol.route.route.path))
|
|
436
|
+
.toEqual(['/project/:id/files/content', '/project/:id/files/content', '/project/:id/files/content'])
|
|
437
|
+
expect([get, save, remove, meta].map(protocol => protocol.route.route.method))
|
|
438
|
+
.toEqual([RouteMethod.GET, RouteMethod.POST, RouteMethod.DELETE, RouteMethod.GET])
|
|
439
|
+
expect(meta.route.route.path).toBe('/project/:id/files/meta')
|
|
440
|
+
expect([run, restart, stop, rebuild].map(protocol => protocol.route.route.path)).toEqual([
|
|
441
|
+
'/project/:id/sandbox/run', '/project/:id/sandbox/restart', '/project/:id/sandbox/stop', '/project/:id/sandbox/rebuild',
|
|
442
|
+
])
|
|
443
|
+
expect(paid.slot.list.route.route.path).toBe('/slot')
|
|
444
|
+
expect(paid.slot.list.route.route.method).toBe(RouteMethod.GET)
|
|
445
|
+
expect(leaves.map(protocol => aliasOf(protocol.route.route.parent))).toEqual(leaves.map(() => connect.base))
|
|
446
|
+
expect(leaves.map(protocol => protocol.gate)).toEqual(leaves.map(() => undefined))
|
|
447
|
+
expect([connectRef.files.save.alias, connectRef.sandbox.rebuild.alias, connectRef.slot.list.alias])
|
|
448
|
+
.toEqual([connect.files.save, connect.sandbox.rebuild, connect.slot.list])
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('the file query, the write body and the metadata query are closed shapes', () => {
|
|
452
|
+
const ajv = new Ajv({ strict: false })
|
|
453
|
+
const query = ajv.compile(ConnectFileQuerySchema)
|
|
454
|
+
const body = ajv.compile(ConnectFileSaveBodySchema)
|
|
455
|
+
const meta = ajv.compile(ConnectFileMetaQuerySchema)
|
|
456
|
+
|
|
457
|
+
expect(query({ path: 'sources/web/src/app.tsx' })).toBe(true)
|
|
458
|
+
expect(query({ path: '' })).toBe(false)
|
|
459
|
+
expect(query({ path: 'x'.repeat(CONNECT_FILE_PATH_MAX + 1) })).toBe(false)
|
|
460
|
+
expect(query({})).toBe(false)
|
|
461
|
+
expect(body({ path: 'README.md', content: '' })).toBe(true)
|
|
462
|
+
expect(body({ path: 'README.md' })).toBe(false)
|
|
463
|
+
expect(body({ path: 'README.md', content: 'x', mode: 'append' })).toBe(false)
|
|
464
|
+
expect(meta({ kind: 'stories' })).toBe(true)
|
|
465
|
+
expect(meta({ kind: 'meta', category: 'ux' })).toBe(true)
|
|
466
|
+
expect(meta({ kind: 'meta', category: null })).toBe(true)
|
|
467
|
+
expect(meta({ kind: 'sources' })).toBe(false)
|
|
468
|
+
expect(meta({ kind: 'all', category: 'design' })).toBe(false)
|
|
469
|
+
})
|
|
470
|
+
test('the inference preference: the person\'s under the account base, the project\'s under the owned base — read free, set paid', () => {
|
|
471
|
+
const paid = connectProtocols({
|
|
472
|
+
guard: DEFAULT_GUARD,
|
|
473
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
474
|
+
accountGate: { alias: 'test-gate', params: ['account'] },
|
|
475
|
+
localLlm: { alias: 'test-paid-llm', params: ['cap'] },
|
|
476
|
+
})
|
|
477
|
+
const { account, project } = paid
|
|
478
|
+
|
|
479
|
+
expect([account.llm.get, account.llm.set, project.llm.get, project.llm.set]
|
|
480
|
+
.map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`))
|
|
481
|
+
.toEqual(['GET /llm', 'POST /llm', 'GET /project/:id/llm', 'POST /project/:id/llm'])
|
|
482
|
+
expect([account.llm.get, account.llm.set].map(protocol => aliasOf(protocol.route.route.parent)))
|
|
483
|
+
.toEqual([connect.account.base, connect.account.base])
|
|
484
|
+
expect([project.llm.get, project.llm.set].map(protocol => aliasOf(protocol.route.route.parent)))
|
|
485
|
+
.toEqual([connect.base, connect.base])
|
|
486
|
+
expect([account.llm.get.gate, project.llm.get.gate]).toEqual([undefined, undefined])
|
|
487
|
+
expect([account.llm.set.gate?.alias, project.llm.set.gate?.alias]).toEqual(['test-paid-llm', 'test-paid-llm'])
|
|
488
|
+
expect([connectRef.account.llm.set.alias, connectRef.project.llm.set.alias])
|
|
489
|
+
.toEqual([connect.account.llm.set, connect.project.llm.set])
|
|
490
|
+
|
|
491
|
+
const ajv = new Ajv({ strict: false })
|
|
492
|
+
const accountBody = ajv.compile(ConnectLlmBodySchema)
|
|
493
|
+
const projectBody = ajv.compile(ConnectProjectLlmBodySchema)
|
|
494
|
+
expect(accountBody({ llmMode: 'local' })).toBe(true)
|
|
495
|
+
expect(accountBody({ llmMode: null })).toBe(false)
|
|
496
|
+
expect(projectBody({ llmMode: 'cloud' })).toBe(true)
|
|
497
|
+
// `null` inherits the person's preference — it is a value, not an omission.
|
|
498
|
+
expect(projectBody({ llmMode: null })).toBe(true)
|
|
499
|
+
expect(projectBody({})).toBe(false)
|
|
500
|
+
expect(projectBody({ llmMode: 'inherit' })).toBe(false)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
test('access tokens are listed and revoked under the account base, never minted, and never under the browser\'s /tokens mount', () => {
|
|
504
|
+
const { list, revoke } = tree.account.tokens
|
|
505
|
+
|
|
506
|
+
expect(Object.keys(tree.account.tokens)).toEqual(['list', 'revoke'])
|
|
507
|
+
expect(Object.keys(connect.account.tokens)).toEqual(['list', 'revoke'])
|
|
508
|
+
expect([list, revoke].map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`))
|
|
509
|
+
.toEqual(['GET /access-tokens', 'POST /access-tokens/:id/revoke'])
|
|
510
|
+
expect([list, revoke].map(protocol => aliasOf(protocol.route.route.parent))).toEqual([connect.account.base, connect.account.base])
|
|
511
|
+
const params = new Ajv({ strict: false }).compile(ConnectAccessTokenParamsSchema)
|
|
512
|
+
expect(params({ id: 'token-1' })).toBe(true)
|
|
513
|
+
expect(params({ id: '' })).toBe(false)
|
|
514
|
+
expect(params({ id: 't'.repeat(CONNECT_TOKEN_ID_MAX + 1) })).toBe(false)
|
|
515
|
+
expect(params({ id: 'token-1', name: 'x' })).toBe(false)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
test('marketing consents are read and withdrawn under the account base — a body that cannot grant', () => {
|
|
519
|
+
const { status, withdraw } = tree.account.privacy
|
|
520
|
+
|
|
521
|
+
expect(Object.keys(tree.account.privacy)).toEqual(['status', 'withdraw'])
|
|
522
|
+
expect([status, withdraw].map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`))
|
|
523
|
+
.toEqual(['GET /privacy', 'POST /privacy/withdraw'])
|
|
524
|
+
expect([status, withdraw].map(protocol => aliasOf(protocol.route.route.parent))).toEqual([connect.account.base, connect.account.base])
|
|
525
|
+
const body = new Ajv({ strict: false }).compile(ConnectPrivacyWithdrawBodySchema)
|
|
526
|
+
expect(body({ keys: ['marketing.email'] })).toBe(true)
|
|
527
|
+
expect(body({ keys: [] })).toBe(false)
|
|
528
|
+
expect(body({ keys: ['marketing.email', 'marketing.email'] })).toBe(false)
|
|
529
|
+
expect(body({ keys: Array.from({ length: CONNECT_PRIVACY_KEYS_MAX + 1 }, (_, at) => `k${at}`) })).toBe(false)
|
|
530
|
+
expect(body({ keys: ['marketing.email'], granted: true })).toBe(false)
|
|
531
|
+
expect(body({ decisions: [{ key: 'marketing.email', granted: true }] })).toBe(false)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
test('a stashed prompt is collected under the account base with the guest pickup\'s reference shape', () => {
|
|
535
|
+
const { pickup } = tree.account.intent
|
|
536
|
+
|
|
537
|
+
expect(`${pickup.route.route.method!.toUpperCase()} ${pickup.route.route.path}`).toBe('POST /intent/pickup')
|
|
538
|
+
expect(aliasOf(pickup.route.route.parent)).toBe(connect.account.base)
|
|
539
|
+
const body = new Ajv({ strict: false }).compile(ConnectIntentPickupBodySchema)
|
|
540
|
+
expect(body({ ref: 'A'.repeat(24) })).toBe(true)
|
|
541
|
+
expect(body({ ref: '0'.repeat(24) })).toBe(false)
|
|
542
|
+
expect(body({ ref: 'A'.repeat(23) })).toBe(false)
|
|
543
|
+
expect(body({ ref: 'A'.repeat(24), prompt: 'x' })).toBe(false)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
test('git and GitHub sit under the owned base, no paid gate, and nothing completes a GitHub authorization', () => {
|
|
547
|
+
const paid = connectProtocols({
|
|
548
|
+
guard: DEFAULT_GUARD,
|
|
549
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
550
|
+
paid: Object.fromEntries(Object.values(ConnectPaidGate).map(kind => [kind, { alias: `gate-${kind}`, params: [kind] }])),
|
|
551
|
+
})
|
|
552
|
+
const routes = [...Object.values(paid.git), ...Object.values(paid.github)]
|
|
553
|
+
|
|
554
|
+
expect(routes.map(protocol => aliasOf(protocol.route.route.parent))).toEqual(routes.map(() => connect.base))
|
|
555
|
+
expect(routes.map(protocol => protocol.gate)).toEqual(routes.map(() => undefined))
|
|
556
|
+
expect(Object.keys(connect.git)).toEqual(['status', 'log', 'commit', 'discard', 'revert'])
|
|
557
|
+
expect(Object.keys(connect.github))
|
|
558
|
+
.toEqual(['authorize', 'publish', 'push', 'pull', 'disconnect', 'repos', 'branches', 'link'])
|
|
559
|
+
// Authorized here, completed only by the web application GitHub returns to.
|
|
560
|
+
const everyAlias = [...Object.values(connect.git), ...Object.values(connect.github)]
|
|
561
|
+
expect(everyAlias.filter(alias => /complete|callback|token/.test(alias))).toEqual([])
|
|
562
|
+
expect(routes.filter(protocol => /complete|callback/.test(protocol.route.route.path)).map(protocol => protocol.alias)).toEqual([])
|
|
563
|
+
expect([connectRef.git.status.alias, connectRef.github.authorize.alias, connectRef.github.link.alias])
|
|
564
|
+
.toEqual([connect.git.status, connect.github.authorize, connect.github.link])
|
|
565
|
+
|
|
566
|
+
const ajv = new Ajv({ strict: false })
|
|
567
|
+
const commit = ajv.compile(ConnectGitCommitBodySchema)
|
|
568
|
+
expect(commit({ message: 'feat: x' })).toBe(true)
|
|
569
|
+
expect(commit({ message: '' })).toBe(false)
|
|
570
|
+
expect(commit({ message: 'x'.repeat(201) })).toBe(false)
|
|
571
|
+
const revert = ajv.compile(ConnectGitRevertBodySchema)
|
|
572
|
+
expect(revert({ hash: 'abc1234' })).toBe(true)
|
|
573
|
+
expect(revert({ hash: 'abc12' })).toBe(false)
|
|
574
|
+
expect(revert({ hash: '--hard' })).toBe(false)
|
|
575
|
+
const publish = ajv.compile(ConnectGithubPublishBodySchema)
|
|
576
|
+
expect(publish({})).toBe(true)
|
|
577
|
+
expect(publish({ repoName: 'shop', private: false })).toBe(true)
|
|
578
|
+
expect(publish({ existing: { owner: 'acme', repo: 'shop' } })).toBe(true)
|
|
579
|
+
expect(publish({ existing: { owner: 'acme' } })).toBe(false)
|
|
580
|
+
// No field a credential could travel in.
|
|
581
|
+
expect(publish({ token: 'ghp_x' })).toBe(false)
|
|
582
|
+
const repos = ajv.compile(ConnectGithubRepoQuerySchema)
|
|
583
|
+
expect(repos({ page: 2, search: 'shop' })).toBe(true)
|
|
584
|
+
expect(repos({ page: 0 })).toBe(false)
|
|
585
|
+
const branches = ajv.compile(ConnectGithubBranchQuerySchema)
|
|
586
|
+
expect(branches({ owner: 'acme', repo: 'shop' })).toBe(true)
|
|
587
|
+
expect(branches({ owner: 'acme' })).toBe(false)
|
|
588
|
+
const link = ajv.compile(ConnectGithubLinkBodySchema)
|
|
589
|
+
expect(link({ owner: 'acme', repo: 'shop', branch: 'main' })).toBe(true)
|
|
590
|
+
expect(link({ owner: 'acme', repo: 'shop', branch: '' })).toBe(false)
|
|
591
|
+
})
|
|
592
|
+
test('the production workload sits under the owned base, each route with exactly its browser twin\'s payment kind', () => {
|
|
593
|
+
const paid = connectProtocols({
|
|
594
|
+
guard: DEFAULT_GUARD,
|
|
595
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
596
|
+
paid: Object.fromEntries(Object.values(ConnectPaidGate).map(kind => [kind, { alias: `gate-${kind}`, params: [kind] }])),
|
|
597
|
+
})
|
|
598
|
+
const { production } = paid
|
|
599
|
+
const routes = [
|
|
600
|
+
production.status, production.publish, production.restart, production.stop,
|
|
601
|
+
production.domain.attach, production.domain.verify, production.domain.detach,
|
|
602
|
+
production.auth.get, production.auth.redirects,
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
expect(routes.map(protocol => aliasOf(protocol.route.route.parent))).toEqual(routes.map(() => connect.base))
|
|
606
|
+
expect(routes.map(protocol => protocol.gate?.alias)).toEqual([
|
|
607
|
+
undefined, 'gate-published-sites', undefined, undefined,
|
|
608
|
+
'gate-custom-domain', undefined, undefined,
|
|
609
|
+
'gate-production-standalone', 'gate-production-standalone',
|
|
610
|
+
])
|
|
611
|
+
expect(routes.map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`)).toEqual([
|
|
612
|
+
'GET /project/:id/production', 'POST /project/:id/production/publish', 'POST /project/:id/production/restart',
|
|
613
|
+
'POST /project/:id/production/stop', 'POST /project/:id/production/domain/attach',
|
|
614
|
+
'POST /project/:id/production/domain/verify', 'POST /project/:id/production/domain/detach',
|
|
615
|
+
'GET /project/:id/production/auth', 'POST /project/:id/production/auth/redirects',
|
|
616
|
+
])
|
|
617
|
+
// No route reveals the sign-in's secret, and there is no run verb: a stopped site comes back by restart.
|
|
618
|
+
const everyAlias = routes.map(protocol => protocol.alias)
|
|
619
|
+
expect(everyAlias.filter(alias => /secret|reveal|:run$/.test(alias))).toEqual([])
|
|
620
|
+
expect([connectRef.production.publish.alias, connectRef.production.domain.attach.alias, connectRef.production.auth.get.alias])
|
|
621
|
+
.toEqual([connect.production.publish, connect.production.domain.attach, connect.production.auth.get])
|
|
622
|
+
|
|
623
|
+
const ajv = new Ajv({ strict: false })
|
|
624
|
+
const domain = ajv.compile(ConnectProductionDomainBodySchema)
|
|
625
|
+
expect(domain({ domain: 'shop.example.com' })).toBe(true)
|
|
626
|
+
expect(domain({ domain: 'localhost' })).toBe(false)
|
|
627
|
+
expect(domain({ domain: 'https://shop.example.com' })).toBe(false)
|
|
628
|
+
expect(domain({ domain: 'shop.example.com', provider: 'x' })).toBe(false)
|
|
629
|
+
const redirects = ajv.compile(ConnectProductionRedirectsBodySchema)
|
|
630
|
+
expect(redirects({ redirects: [] })).toBe(true)
|
|
631
|
+
expect(redirects({ redirects: ['https://self.example.com/dispatcher'] })).toBe(true)
|
|
632
|
+
expect(redirects({ redirects: Array.from({ length: CONNECT_REDIRECTS_MAX + 1 }, (_, i) => `https://h${i}.test`) })).toBe(false)
|
|
633
|
+
expect(redirects({ redirects: ['x'.repeat(2049)] })).toBe(false)
|
|
634
|
+
expect(redirects({})).toBe(false)
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
test('the three feeds are GET reads by cursor — the project\'s under the owned base, the notices under the account base, no paid gate', () => {
|
|
638
|
+
const paid = connectProtocols({
|
|
639
|
+
guard: DEFAULT_GUARD,
|
|
640
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
641
|
+
accountGate: { alias: 'test-gate', params: ['account'] },
|
|
642
|
+
paid: Object.fromEntries(Object.values(ConnectPaidGate).map(kind => [kind, { alias: `gate-${kind}`, params: [kind] }])),
|
|
643
|
+
})
|
|
644
|
+
const routes = [paid.project.activity, paid.account.notifications, paid.files.changes]
|
|
645
|
+
|
|
646
|
+
expect(routes.map(protocol => protocol.alias))
|
|
647
|
+
.toEqual([connect.project.activity, connect.account.notifications, connect.files.changes])
|
|
648
|
+
expect(routes.map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`))
|
|
649
|
+
.toEqual(['GET /project/:id/activity', 'GET /notifications', 'GET /project/:id/files/changes'])
|
|
650
|
+
expect(routes.map(protocol => aliasOf(protocol.route.route.parent)))
|
|
651
|
+
.toEqual([connect.base, connect.account.base, connect.base])
|
|
652
|
+
expect(routes.map(protocol => protocol.gate)).toEqual([undefined, undefined, undefined])
|
|
653
|
+
// Cursor polling, never a socket — the connector holds no connection open.
|
|
654
|
+
expect(routes.every(protocol => protocol.route.route.protocol !== RouteProtocols.SOCKET)).toBe(true)
|
|
655
|
+
expect([connectRef.project.activity.alias, connectRef.account.notifications.alias, connectRef.files.changes.alias])
|
|
656
|
+
.toEqual([connect.project.activity, connect.account.notifications, connect.files.changes])
|
|
657
|
+
expect([CONNECT_FEED_WAIT_MAX_SEC, CONNECT_FEED_LIMIT_MAX, CONNECT_FEED_START]).toEqual([20, 200, '0-0'])
|
|
658
|
+
expect(Object.values(ConnectFeedDetail)).toEqual(['progress', 'thinking'])
|
|
659
|
+
expect(Object.values(ConnectFeedKind)).toContain('thinking')
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
test('a feed query is a cursor, a bounded limit and a wait of at most twenty seconds', () => {
|
|
663
|
+
const ajv = new Ajv({ strict: false })
|
|
664
|
+
const query = ajv.compile(ConnectFeedQuerySchema)
|
|
665
|
+
const activity = ajv.compile(ConnectActivityQuerySchema)
|
|
666
|
+
|
|
667
|
+
expect(query({})).toBe(true)
|
|
668
|
+
expect(query({ after: '1759800000000-0', limit: 10, wait: CONNECT_FEED_WAIT_MAX_SEC })).toBe(true)
|
|
669
|
+
expect(query({ after: CONNECT_FEED_START })).toBe(true)
|
|
670
|
+
expect(query({ after: null, limit: null, wait: null })).toBe(true)
|
|
671
|
+
expect(query({ wait: CONNECT_FEED_WAIT_MAX_SEC + 1 })).toBe(false)
|
|
672
|
+
expect(query({ limit: 0 })).toBe(false)
|
|
673
|
+
expect(query({ limit: CONNECT_FEED_LIMIT_MAX + 1 })).toBe(false)
|
|
674
|
+
expect(query({ after: '$' })).toBe(false)
|
|
675
|
+
expect(query({ after: '+' })).toBe(false)
|
|
676
|
+
expect(query({ after: '1-0', detail: 'thinking' })).toBe(false)
|
|
677
|
+
expect(activity({ detail: 'thinking' })).toBe(true)
|
|
678
|
+
expect(activity({ detail: null })).toBe(true)
|
|
679
|
+
expect(activity({ detail: 'everything' })).toBe(false)
|
|
680
|
+
expect(activity({ after: '1-0', wait: 5, detail: 'progress' })).toBe(true)
|
|
681
|
+
})
|
|
144
682
|
})
|