@jskit-ai/kernel 0.1.244 → 0.1.246

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 (101) hide show
  1. package/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +18 -76
  4. package/client/index.js +7 -2
  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 +38 -10
  9. package/client/shellBootstrap.test.js +85 -6
  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 +14 -58
  15. package/package.json +2 -4
  16. package/server/actions/actionCatalogue.js +13 -34
  17. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  18. package/{shared → server}/actions/actionDefinitions.js +24 -131
  19. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  20. package/server/actions/actionEvents.js +97 -2
  21. package/server/actions/actionEvents.test.js +79 -0
  22. package/{shared → server}/actions/executionContext.js +3 -13
  23. package/server/actions/idempotency.js +24 -0
  24. package/server/actions/pipeline.js +87 -0
  25. package/{shared → server}/actions/policies.js +20 -33
  26. package/{shared → server}/actions/policies.test.js +6 -6
  27. package/{shared → server}/actions/registry.js +3 -22
  28. package/{shared → server}/actions/registry.test.js +70 -32
  29. package/server/actions/requestMeta.js +17 -0
  30. package/server/http/capabilityHttpRuntime.js +1 -1
  31. package/server/http/capabilityHttpRuntime.test.js +56 -0
  32. package/server/http/errorHandling.js +291 -0
  33. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  34. package/server/http/lib/routeRegistration.js +36 -150
  35. package/server/http/lib/routeTransport.js +2 -0
  36. package/server/http/lib/routeValidator.js +5 -47
  37. package/server/http/lib/routeValidator.test.js +15 -17
  38. package/server/http/lib/router.js +10 -94
  39. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  40. package/server/platform/providerRuntime/helpers.js +1 -34
  41. package/server/runtime/integers.js +1 -5
  42. package/server/runtime/serviceAuthorization.js +4 -9
  43. package/server/support/appConfig.js +2 -1
  44. package/server/support/appConfigFiles.js +1 -38
  45. package/server/support/appConfigFiles.test.js +3 -39
  46. package/server/support/index.js +1 -20
  47. package/shared/capabilities/defineProvider.js +1 -1
  48. package/shared/capabilities/index.js +1 -1
  49. package/shared/index.js +1 -1
  50. package/shared/support/index.js +1 -4
  51. package/shared/support/mobileConfig.js +99 -0
  52. package/shared/support/mobileConfig.test.js +113 -0
  53. package/shared/support/normalize.js +0 -108
  54. package/shared/support/normalize.test.js +0 -134
  55. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  56. package/shared/support/returnToPath.js +16 -20
  57. package/shared/support/returnToPath.test.js +5 -33
  58. package/shared/support/shellLayoutTargets.js +2 -140
  59. package/shared/support/shellLayoutTargets.test.js +1 -110
  60. package/shared/support/sorting.js +1 -9
  61. package/shared/surface/apiPaths.js +1 -82
  62. package/shared/surface/paths.js +38 -171
  63. package/shared/surface/registry.js +2 -1
  64. package/shared/surface/runtime.js +18 -109
  65. package/shared/surface/runtime.test.js +57 -0
  66. package/shared/validators/index.js +1 -4
  67. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  68. package/shared/validators/recordIdParamsValidator.js +0 -6
  69. package/shared/validators/schemaDefinitions.js +4 -30
  70. package/shared/validators/schemaPayloadValidation.js +2 -6
  71. package/test/barrelExposure.test.js +5 -8
  72. package/test/exportsContract.test.js +1 -3
  73. package/client/mobileLaunchRouting.js +0 -231
  74. package/client/mobileLaunchRouting.test.js +0 -230
  75. package/server/http/lib/controller.js +0 -183
  76. package/server/http/lib/controller.test.js +0 -143
  77. package/server/http/lib/index.js +0 -5
  78. package/server/runtime/canonicalJson.js +0 -74
  79. package/server/runtime/entityChangeEvents.js +0 -307
  80. package/server/runtime/entityChangeEvents.test.js +0 -274
  81. package/server/runtime/fastifyBootstrap.js +0 -553
  82. package/server/runtime/moduleConfig.js +0 -223
  83. package/server/runtime/moduleConfig.test.js +0 -165
  84. package/server/runtime/pagination.js +0 -13
  85. package/server/runtime/realtimeNormalization.js +0 -21
  86. package/server/runtime/routeUtils.js +0 -20
  87. package/server/runtime/securityAudit.js +0 -269
  88. package/server/runtime/securityAudit.test.js +0 -41
  89. package/server/support/pageTargets.js +0 -772
  90. package/server/support/pageTargets.test.js +0 -659
  91. package/server/support/shellOutlets.js +0 -576
  92. package/server/support/shellOutlets.test.js +0 -336
  93. package/server/surface/index.js +0 -18
  94. package/shared/actions/audit.js +0 -7
  95. package/shared/actions/idempotency.js +0 -62
  96. package/shared/actions/observability.js +0 -10
  97. package/shared/actions/pipeline.js +0 -309
  98. package/shared/actions/requestMeta.js +0 -36
  99. package/shared/actions/textNormalization.js +0 -3
  100. package/shared/actions/withActionDefaults.js +0 -34
  101. /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
- });