@jskit-ai/kernel 0.1.245 → 0.1.247

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +13 -75
  4. package/client/index.js +1 -1
  5. package/client/logging.js +8 -2
  6. package/client/moduleBootstrap.js +41 -33
  7. package/client/moduleBootstrap.test.js +35 -0
  8. package/client/shellBootstrap.js +1 -8
  9. package/client/shellBootstrap.test.js +32 -5
  10. package/client/shellRouting.js +16 -61
  11. package/client/shellRouting.test.js +30 -1
  12. package/client/vite/clientBootstrapPlugin.js +0 -6
  13. package/client/vite/clientBootstrapPlugin.test.js +2 -3
  14. package/internal/node/installedPackages.js +19 -62
  15. package/internal/node/installedPackages.test.js +33 -0
  16. package/package.json +2 -4
  17. package/server/actions/actionCatalogue.js +13 -34
  18. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  19. package/{shared → server}/actions/actionDefinitions.js +24 -131
  20. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  21. package/server/actions/actionEvents.js +97 -2
  22. package/server/actions/actionEvents.test.js +79 -0
  23. package/{shared → server}/actions/executionContext.js +3 -13
  24. package/server/actions/idempotency.js +24 -0
  25. package/server/actions/pipeline.js +87 -0
  26. package/{shared → server}/actions/policies.js +20 -33
  27. package/{shared → server}/actions/policies.test.js +6 -6
  28. package/{shared → server}/actions/registry.js +3 -22
  29. package/{shared → server}/actions/registry.test.js +70 -32
  30. package/server/actions/requestMeta.js +17 -0
  31. package/server/http/capabilityHttpRuntime.js +1 -1
  32. package/server/http/capabilityHttpRuntime.test.js +56 -0
  33. package/server/http/errorHandling.js +291 -0
  34. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  35. package/server/http/lib/routeRegistration.js +36 -150
  36. package/server/http/lib/routeTransport.js +2 -0
  37. package/server/http/lib/routeValidator.js +5 -47
  38. package/server/http/lib/routeValidator.test.js +15 -17
  39. package/server/http/lib/router.js +10 -94
  40. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  41. package/server/platform/providerRuntime/helpers.js +1 -34
  42. package/server/runtime/integers.js +1 -5
  43. package/server/runtime/serviceAuthorization.js +4 -9
  44. package/server/support/appConfig.js +2 -1
  45. package/server/support/appConfigFiles.js +1 -38
  46. package/server/support/appConfigFiles.test.js +3 -39
  47. package/server/support/index.js +1 -20
  48. package/shared/capabilities/defineProvider.js +1 -1
  49. package/shared/capabilities/index.js +1 -1
  50. package/shared/index.js +1 -1
  51. package/shared/support/index.js +1 -4
  52. package/shared/support/mobileConfig.js +99 -0
  53. package/shared/support/mobileConfig.test.js +113 -0
  54. package/shared/support/normalize.js +0 -108
  55. package/shared/support/normalize.test.js +0 -134
  56. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  57. package/shared/support/returnToPath.js +1 -20
  58. package/shared/support/returnToPath.test.js +1 -36
  59. package/shared/support/shellLayoutTargets.js +2 -140
  60. package/shared/support/shellLayoutTargets.test.js +1 -110
  61. package/shared/support/sorting.js +1 -9
  62. package/shared/surface/apiPaths.js +1 -82
  63. package/shared/surface/paths.js +38 -171
  64. package/shared/surface/registry.js +2 -1
  65. package/shared/surface/runtime.js +18 -109
  66. package/shared/surface/runtime.test.js +57 -0
  67. package/shared/validators/index.js +1 -4
  68. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  69. package/shared/validators/recordIdParamsValidator.js +0 -6
  70. package/shared/validators/schemaDefinitions.js +4 -30
  71. package/shared/validators/schemaPayloadValidation.js +2 -6
  72. package/test/barrelExposure.test.js +3 -7
  73. package/test/exportsContract.test.js +1 -3
  74. package/client/mobileLaunchRouting.js +0 -231
  75. package/client/mobileLaunchRouting.test.js +0 -230
  76. package/server/http/lib/controller.js +0 -183
  77. package/server/http/lib/controller.test.js +0 -143
  78. package/server/http/lib/index.js +0 -5
  79. package/server/runtime/canonicalJson.js +0 -74
  80. package/server/runtime/entityChangeEvents.js +0 -307
  81. package/server/runtime/entityChangeEvents.test.js +0 -274
  82. package/server/runtime/fastifyBootstrap.js +0 -553
  83. package/server/runtime/moduleConfig.js +0 -223
  84. package/server/runtime/moduleConfig.test.js +0 -165
  85. package/server/runtime/pagination.js +0 -13
  86. package/server/runtime/realtimeNormalization.js +0 -21
  87. package/server/runtime/routeUtils.js +0 -20
  88. package/server/runtime/securityAudit.js +0 -269
  89. package/server/runtime/securityAudit.test.js +0 -41
  90. package/server/support/pageTargets.js +0 -772
  91. package/server/support/pageTargets.test.js +0 -659
  92. package/server/support/shellOutlets.js +0 -576
  93. package/server/support/shellOutlets.test.js +0 -336
  94. package/server/surface/index.js +0 -18
  95. package/shared/actions/audit.js +0 -7
  96. package/shared/actions/idempotency.js +0 -62
  97. package/shared/actions/observability.js +0 -10
  98. package/shared/actions/pipeline.js +0 -309
  99. package/shared/actions/requestMeta.js +0 -36
  100. package/shared/actions/textNormalization.js +0 -3
  101. package/shared/actions/withActionDefaults.js +0 -34
  102. /package/{shared → server}/actions/executionContext.test.js +0 -0
@@ -1,307 +0,0 @@
1
- import { normalizeObject, normalizeOpaqueId, normalizeText } from "../../shared/support/normalize.js";
2
- import { resolveServiceContext } from "./serviceAuthorization.js";
3
-
4
- const ENTITY_CHANGE_OPERATIONS = new Set(["created", "updated", "deleted"]);
5
-
6
- function resolveContextScope(context = {}) {
7
- const sourceScope = normalizeObject(context?.scope || context?.requestMeta?.scope || context?.request?.scope);
8
- const kind = normalizeText(sourceScope.kind).toLowerCase();
9
- if (!kind) {
10
- return null;
11
- }
12
- if (kind === "global") {
13
- return {
14
- kind: "global",
15
- id: null
16
- };
17
- }
18
-
19
- const scopeId = normalizeOpaqueId(sourceScope.id);
20
- if (scopeId == null) {
21
- return null;
22
- }
23
-
24
- const resolvedScope = {
25
- kind,
26
- id: scopeId
27
- };
28
-
29
- const scopeUserId = normalizeOpaqueId(sourceScope.userId);
30
- if (scopeUserId != null) {
31
- resolvedScope.userId = scopeUserId;
32
- }
33
-
34
- const scopedScopeId = normalizeOpaqueId(sourceScope.scopeId);
35
- if (scopedScopeId != null) {
36
- resolvedScope.scopeId = scopedScopeId;
37
- }
38
-
39
- return resolvedScope;
40
- }
41
-
42
- function resolveVisibilityScope(visibilityContext = {}, runtimeContext = {}) {
43
- const visibility = normalizeText(visibilityContext.visibility).toLowerCase();
44
- const scopeKind = normalizeText(visibilityContext.scopeKind || visibility).toLowerCase();
45
- const scopeOwnerId = normalizeOpaqueId(visibilityContext.scopeOwnerId);
46
- const userId = normalizeOpaqueId(visibilityContext.userId);
47
- const requiresActorScope = visibilityContext.requiresActorScope === true;
48
-
49
- if (requiresActorScope && userId == null) {
50
- return null;
51
- }
52
-
53
- if (scopeKind && scopeOwnerId != null) {
54
- const scope = {
55
- kind: scopeKind,
56
- id: scopeOwnerId
57
- };
58
- if (requiresActorScope) {
59
- scope.scopeId = scopeOwnerId;
60
- scope.userId = userId;
61
- }
62
- return scope;
63
- }
64
- if (scopeKind && scopeKind !== "user") {
65
- return null;
66
- }
67
-
68
- if (!scopeKind && scopeOwnerId != null) {
69
- return {
70
- kind: "scope",
71
- id: scopeOwnerId
72
- };
73
- }
74
-
75
- if (scopeKind === "user" && userId != null) {
76
- return {
77
- kind: "user",
78
- id: userId
79
- };
80
- }
81
-
82
- return null;
83
- }
84
-
85
- function resolveDefaultScope(visibilityContext = {}, runtime = {}) {
86
- const runtimeContext = normalizeObject(runtime?.context);
87
-
88
- const visibilityScope = resolveVisibilityScope(visibilityContext, runtimeContext);
89
- if (visibilityScope) {
90
- return visibilityScope;
91
- }
92
-
93
- const contextScope = resolveContextScope(runtimeContext);
94
- if (contextScope) {
95
- return contextScope;
96
- }
97
-
98
- return {
99
- kind: "global",
100
- id: null
101
- };
102
- }
103
-
104
- function resolveCommandId(requestMeta = {}) {
105
- return normalizeText(requestMeta.commandId || requestMeta.idempotencyKey || "") || null;
106
- }
107
-
108
- function resolveSourceClientId(requestMeta = {}) {
109
- return normalizeText(requestMeta.sourceClientId) || null;
110
- }
111
-
112
- function createEntityChangePublisher({
113
- domainEvents,
114
- source,
115
- entity,
116
- scopeResolver = resolveDefaultScope
117
- } = {}) {
118
- if (!domainEvents || typeof domainEvents.publish !== "function") {
119
- throw new TypeError("createEntityChangePublisher requires domainEvents.publish().");
120
- }
121
-
122
- const normalizedSource = normalizeText(source);
123
- if (!normalizedSource) {
124
- throw new TypeError("createEntityChangePublisher requires source.");
125
- }
126
-
127
- const normalizedEntity = normalizeText(entity);
128
- if (!normalizedEntity) {
129
- throw new TypeError("createEntityChangePublisher requires entity.");
130
- }
131
-
132
- return async function publishEntityChange(operation, entityId, options = {}, meta = null) {
133
- const normalizedOperation = normalizeText(operation).toLowerCase();
134
- if (!ENTITY_CHANGE_OPERATIONS.has(normalizedOperation)) {
135
- throw new TypeError("publishEntityChange operation must be one of: created, updated, deleted.");
136
- }
137
-
138
- const normalizedEntityId = normalizeOpaqueId(entityId);
139
- if (normalizedEntityId == null) {
140
- return null;
141
- }
142
-
143
- const context = resolveServiceContext(options);
144
- const requestMeta = normalizeObject(context.requestMeta);
145
- const visibilityContext = normalizeObject(options.visibilityContext || context.visibilityContext);
146
- const scope = scopeResolver(visibilityContext, {
147
- context,
148
- options
149
- });
150
-
151
- const payload = {
152
- source: normalizedSource,
153
- entity: normalizedEntity,
154
- operation: normalizedOperation,
155
- entityId: normalizedEntityId,
156
- scope: scope && typeof scope === "object" ? scope : resolveDefaultScope(visibilityContext),
157
- actorId: normalizeOpaqueId(context?.actor?.id),
158
- commandId: resolveCommandId(requestMeta),
159
- sourceClientId: resolveSourceClientId(requestMeta),
160
- occurredAt: new Date().toISOString()
161
- };
162
- const normalizedMeta = normalizeObject(meta);
163
- if (Object.keys(normalizedMeta).length > 0) {
164
- payload.meta = normalizedMeta;
165
- }
166
-
167
- await domainEvents.publish(payload);
168
- return payload;
169
- };
170
- }
171
-
172
- function normalizeMetaTextField(source = {}, fieldName = "", { context = "realtime entity change" } = {}) {
173
- if (!Object.hasOwn(source, fieldName)) {
174
- return "";
175
- }
176
-
177
- const value = source[fieldName];
178
- if (value == null) {
179
- return "";
180
- }
181
- if (typeof value !== "string") {
182
- throw new TypeError(`${context}.${fieldName} must be a non-empty string.`);
183
- }
184
-
185
- const normalized = normalizeText(value);
186
- if (!normalized) {
187
- throw new TypeError(`${context}.${fieldName} must be a non-empty string.`);
188
- }
189
- return normalized;
190
- }
191
-
192
- function normalizeRealtimePayload(value, { context = "realtime entity change.payload" } = {}) {
193
- if (value == null) {
194
- return null;
195
- }
196
- if (!value || typeof value !== "object" || Array.isArray(value)) {
197
- throw new TypeError(`${context} must be an object.`);
198
- }
199
- return Object.freeze({
200
- ...value
201
- });
202
- }
203
-
204
- function createRealtimeEntityChangeMeta({
205
- serviceId,
206
- methodName,
207
- event,
208
- change = {}
209
- } = {}) {
210
- const source = normalizeObject(change);
211
- const baseMeta = normalizeObject(source.meta);
212
- const meta = {
213
- ...baseMeta,
214
- service: Object.freeze({
215
- id: serviceId,
216
- method: methodName
217
- })
218
- };
219
-
220
- const action = normalizeMetaTextField(source, "action");
221
- if (action) {
222
- meta.action = action;
223
- }
224
-
225
- const reason = normalizeMetaTextField(source, "reason");
226
- if (reason) {
227
- meta.reason = reason;
228
- }
229
-
230
- const realtimeSource = normalizeObject(source.realtime);
231
- const payloadSource = Object.hasOwn(source, "payload") ? source.payload : realtimeSource.payload;
232
- const payload = normalizeRealtimePayload(payloadSource);
233
- meta.realtime = payload
234
- ? Object.freeze({
235
- event,
236
- payload
237
- })
238
- : Object.freeze({
239
- event
240
- });
241
-
242
- return Object.freeze(meta);
243
- }
244
-
245
- function resolveRealtimeEntityChangeOptions(change = {}, options = {}) {
246
- const source = normalizeObject(change);
247
- if (Object.hasOwn(source, "options")) {
248
- return normalizeObject(source.options);
249
- }
250
- return normalizeObject(options);
251
- }
252
-
253
- function createRealtimeEntityChangePublisher({
254
- domainEvents,
255
- source,
256
- entity,
257
- event,
258
- serviceId,
259
- methodName,
260
- scopeResolver = resolveDefaultScope
261
- } = {}) {
262
- const normalizedEvent = normalizeText(event);
263
- if (!normalizedEvent) {
264
- throw new TypeError("createRealtimeEntityChangePublisher requires event.");
265
- }
266
-
267
- const normalizedServiceId = normalizeText(serviceId);
268
- if (!normalizedServiceId) {
269
- throw new TypeError("createRealtimeEntityChangePublisher requires serviceId.");
270
- }
271
-
272
- const normalizedMethodName = normalizeText(methodName);
273
- if (!normalizedMethodName) {
274
- throw new TypeError("createRealtimeEntityChangePublisher requires methodName.");
275
- }
276
-
277
- const publishEntityChange = createEntityChangePublisher({
278
- domainEvents,
279
- source,
280
- entity,
281
- scopeResolver
282
- });
283
-
284
- return async function publishRealtimeEntityChange(operation, entityId, change = {}, options = {}) {
285
- const runtimeOptions = resolveRealtimeEntityChangeOptions(change, options);
286
- const meta = createRealtimeEntityChangeMeta({
287
- serviceId: normalizedServiceId,
288
- methodName: normalizedMethodName,
289
- event: normalizedEvent,
290
- change
291
- });
292
- return publishEntityChange(operation, entityId, runtimeOptions, meta);
293
- };
294
- }
295
-
296
- function createNoopEntityChangePublisher() {
297
- return async function publishEntityChange() {
298
- return null;
299
- };
300
- }
301
-
302
- export {
303
- resolveDefaultScope,
304
- createEntityChangePublisher,
305
- createRealtimeEntityChangePublisher,
306
- createNoopEntityChangePublisher
307
- };
@@ -1,274 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- createEntityChangePublisher,
5
- createRealtimeEntityChangePublisher
6
- } from "./entityChangeEvents.js";
7
-
8
- test("entity change publisher emits normalized event payload", async () => {
9
- const published = [];
10
- const publishEntityChange = createEntityChangePublisher({
11
- domainEvents: {
12
- async publish(payload) {
13
- published.push(payload);
14
- }
15
- },
16
- source: "crud",
17
- entity: "record"
18
- });
19
-
20
- const payload = await publishEntityChange("created", 5, {
21
- context: {
22
- actor: { id: 17 },
23
- requestMeta: {
24
- commandId: "cmd-1",
25
- sourceClientId: "client-a"
26
- },
27
- visibilityContext: {
28
- scopeOwnerId: 23
29
- }
30
- }
31
- }, {
32
- service: {
33
- token: "crud.customers",
34
- method: "createRecord"
35
- }
36
- });
37
-
38
- assert.equal(payload?.operation, "created");
39
- assert.equal(payload?.entityId, "5");
40
- assert.deepEqual(payload?.scope, { kind: "scope", id: "23" });
41
- assert.equal(payload?.actorId, "17");
42
- assert.equal(payload?.commandId, "cmd-1");
43
- assert.equal(payload?.sourceClientId, "client-a");
44
- assert.equal(payload?.meta?.service?.token, "crud.customers");
45
- assert.equal(payload?.meta?.service?.method, "createRecord");
46
- assert.equal(published.length, 1);
47
- });
48
-
49
- test("entity change publisher accepts opaque entity identifiers", async () => {
50
- const published = [];
51
- const publishEntityChange = createEntityChangePublisher({
52
- domainEvents: {
53
- async publish(payload) {
54
- published.push(payload);
55
- }
56
- },
57
- source: "crud",
58
- entity: "record"
59
- });
60
-
61
- const payload = await publishEntityChange("updated", "record_4f5d2f6a-607e-4b4b-9bfa-4f8c2b1d3c8e");
62
-
63
- assert.equal(payload?.entityId, "record_4f5d2f6a-607e-4b4b-9bfa-4f8c2b1d3c8e");
64
- assert.equal(published.length, 1);
65
- });
66
-
67
- test("entity change publisher ignores missing entity identifiers", async () => {
68
- const published = [];
69
- const publishEntityChange = createEntityChangePublisher({
70
- domainEvents: {
71
- async publish(payload) {
72
- published.push(payload);
73
- }
74
- },
75
- source: "crud",
76
- entity: "record"
77
- });
78
-
79
- const payload = await publishEntityChange("updated", null);
80
-
81
- assert.equal(payload, null);
82
- assert.equal(published.length, 0);
83
- });
84
-
85
- test("entity change publisher infers scoped owner from service context when visibility owner ids are missing", async () => {
86
- const published = [];
87
- const publishEntityChange = createEntityChangePublisher({
88
- domainEvents: {
89
- async publish(payload) {
90
- published.push(payload);
91
- }
92
- },
93
- source: "workspace",
94
- entity: "settings"
95
- });
96
-
97
- const payload = await publishEntityChange(
98
- "updated",
99
- 11,
100
- {
101
- context: {
102
- actor: { id: 17 },
103
- scope: { kind: "workspace", id: 23 },
104
- visibilityContext: {
105
- visibility: "workspace"
106
- }
107
- }
108
- },
109
- {
110
- service: {
111
- token: "users.workspace.settings.service",
112
- method: "updateWorkspaceSettings"
113
- }
114
- }
115
- );
116
-
117
- assert.deepEqual(payload?.scope, { kind: "workspace", id: "23" });
118
- assert.equal(published.length, 1);
119
- });
120
-
121
- test("entity change publisher supports opaque actor and scope identifiers", async () => {
122
- const published = [];
123
- const publishEntityChange = createEntityChangePublisher({
124
- domainEvents: {
125
- async publish(payload) {
126
- published.push(payload);
127
- }
128
- },
129
- source: "workspace",
130
- entity: "settings"
131
- });
132
-
133
- const payload = await publishEntityChange("updated", 11, {
134
- context: {
135
- actor: { id: "user_17" },
136
- visibilityContext: {
137
- scopeKind: "workspace_user",
138
- scopeOwnerId: "workspace_23",
139
- userId: "user_17",
140
- requiresActorScope: true
141
- }
142
- }
143
- });
144
-
145
- assert.deepEqual(payload?.scope, {
146
- kind: "workspace_user",
147
- id: "workspace_23",
148
- scopeId: "workspace_23",
149
- userId: "user_17"
150
- });
151
- assert.equal(payload?.actorId, "user_17");
152
- assert.equal(published.length, 1);
153
- });
154
-
155
- test("entity change publisher does not infer actor-scoped ownership from actor.id", async () => {
156
- const published = [];
157
- const publishEntityChange = createEntityChangePublisher({
158
- domainEvents: {
159
- async publish(payload) {
160
- published.push(payload);
161
- }
162
- },
163
- source: "workspace",
164
- entity: "settings"
165
- });
166
-
167
- const payload = await publishEntityChange("updated", 11, {
168
- context: {
169
- actor: { id: "user_17" },
170
- visibilityContext: {
171
- scopeKind: "workspace_user",
172
- scopeOwnerId: "workspace_23",
173
- requiresActorScope: true
174
- }
175
- }
176
- });
177
-
178
- assert.deepEqual(payload?.scope, {
179
- kind: "global",
180
- id: null
181
- });
182
- assert.equal(payload?.actorId, "user_17");
183
- assert.equal(published.length, 1);
184
- });
185
-
186
- test("entity change publisher does not infer actor scope from scope kind suffix", async () => {
187
- const published = [];
188
- const publishEntityChange = createEntityChangePublisher({
189
- domainEvents: {
190
- async publish(payload) {
191
- published.push(payload);
192
- }
193
- },
194
- source: "workspace",
195
- entity: "settings"
196
- });
197
-
198
- const payload = await publishEntityChange("updated", 11, {
199
- context: {
200
- visibilityContext: {
201
- scopeKind: "workspace_user",
202
- scopeOwnerId: "workspace_23",
203
- requiresActorScope: false
204
- }
205
- }
206
- });
207
-
208
- assert.deepEqual(payload?.scope, {
209
- kind: "workspace_user",
210
- id: "workspace_23"
211
- });
212
- assert.equal(payload?.actorId, null);
213
- assert.equal(published.length, 1);
214
- });
215
-
216
- test("realtime entity change publisher adds service, semantic, and realtime metadata", async () => {
217
- const published = [];
218
- const publishProjectChange = createRealtimeEntityChangePublisher({
219
- domainEvents: {
220
- async publish(payload) {
221
- published.push(payload);
222
- }
223
- },
224
- source: "vibe64",
225
- entity: "project",
226
- event: "vibe64.project.changed",
227
- serviceId: "vibe64.terminals",
228
- methodName: "projectRuntime"
229
- });
230
-
231
- const payload = await publishProjectChange("updated", "project_acme", {
232
- action: "runtime-closed",
233
- reason: "user-request",
234
- payload: {
235
- message: "Project is closed.",
236
- runtime: {
237
- open: false
238
- }
239
- },
240
- options: {
241
- context: {
242
- actor: {
243
- id: "user_17"
244
- },
245
- visibilityContext: {
246
- scopeKind: "workspace",
247
- scopeOwnerId: "workspace_23"
248
- }
249
- }
250
- }
251
- });
252
-
253
- assert.equal(payload?.source, "vibe64");
254
- assert.equal(payload?.entity, "project");
255
- assert.equal(payload?.operation, "updated");
256
- assert.equal(payload?.entityId, "project_acme");
257
- assert.deepEqual(payload?.scope, {
258
- kind: "workspace",
259
- id: "workspace_23"
260
- });
261
- assert.equal(payload?.actorId, "user_17");
262
- assert.equal(payload?.meta?.service?.id, "vibe64.terminals");
263
- assert.equal(payload?.meta?.service?.method, "projectRuntime");
264
- assert.equal(payload?.meta?.action, "runtime-closed");
265
- assert.equal(payload?.meta?.reason, "user-request");
266
- assert.equal(payload?.meta?.realtime?.event, "vibe64.project.changed");
267
- assert.deepEqual(payload?.meta?.realtime?.payload, {
268
- message: "Project is closed.",
269
- runtime: {
270
- open: false
271
- }
272
- });
273
- assert.equal(published.length, 1);
274
- });