@sequenceholdings/artifact-studio 0.6.6 → 0.6.7

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 CHANGED
@@ -17,7 +17,7 @@ const orm = createArtifactOrmExecutor({ namespace: 'team_directory', manifest })
17
17
  const result = await orm.execute('Profiles', { limit: 100, offset: 0 })
18
18
  ```
19
19
 
20
- Declare the exact namespace path in `artifact.bundle.yml`:
20
+ For a static (schema-v1) artifact, declare the exact namespace path in `artifact.bundle.yml`:
21
21
 
22
22
  ```yaml
23
23
  capabilities:
@@ -25,7 +25,7 @@ capabilities:
25
25
  write: ['/api/data/v2/team_directory']
26
26
  ```
27
27
 
28
- Persisted queries and mutations both use POST, so both require this `write`
28
+ In that runtime, persisted queries and mutations both use POST, so both require this `write`
29
29
  declaration. This declaration permits the artifact to make the request; each
30
30
  user still needs namespace runtime access and the operation's role/capability.
31
31
  ORM row policies and write checks enforce that caller's access. The helper uses
@@ -36,10 +36,77 @@ contract for external clients.
36
36
  The adapter validates the namespace and manifest, rejects unknown operations,
37
37
  and returns the operation's `data`. Errors retain `OrmOperationError.status`,
38
38
  `code`, and `errors`; missing host context or malformed success responses fail
39
- instead of reporting saved data. The adapter never retries a call automatically. For
40
- several operations in one traced UI action, pass `withTraceAction`'s client as
39
+ instead of reporting saved data. The adapter never retries a call automatically. On the
40
+ static runtime, for several operations in one traced UI action, pass `withTraceAction`'s client as
41
41
  the optional `sdk` argument.
42
42
 
43
+ ### Next.js browser ORM admission (internal prerelease)
44
+
45
+ The following schema-v2 manifest controls are planned for Artifact Studio 0.6.7
46
+ and Studio CLI 0.4.7. They require the matching platform deployment as well as
47
+ the SDK/CLI release; workspace source and version numbers alone do not establish
48
+ published availability. The Next.js runtime remains an internal, opt-in Preview
49
+ surface.
50
+
51
+ For a Next.js artifact, declare browser table reach and optionally restrict the
52
+ persisted operations and artifact capabilities required to use that reach:
53
+
54
+ ```yaml
55
+ capabilities:
56
+ roles:
57
+ capabilities:
58
+ directory-reader:
59
+ description: Read the team directory in this application
60
+ data:
61
+ read: [team_directory/profiles]
62
+ write: []
63
+ operations: [team_directory/ops/Profiles]
64
+ required_capabilities:
65
+ team_directory: [directory-reader]
66
+ ```
67
+
68
+ `operations` accepts at most 256 exact `namespace/ops/Operation` references;
69
+ wildcards are not accepted. Omitting it preserves existing table-based
70
+ admission. An explicit `operations: []` denies every browser ORM operation.
71
+ Listing an operation does not replace its `data.read` or `data.write` reach:
72
+ queries still need every table they read, and mutations still need write reach.
73
+ The Next.js host routes ORM requests through this data contract; no additional
74
+ API path declaration is needed. The POST transport does not grant mutation authority.
75
+
76
+ The allowlist admits registered persisted operations only. Autogenerated
77
+ `DatasetPreview_*` hashes are excluded even if a registered operation has the
78
+ same name. Operation admission retains the existing active and previous
79
+ operation-set fallback; the allowlist pins names, not one content hash. Give a
80
+ security-tightened replacement query a new operation name and remove the old
81
+ name from the allowlist, otherwise a retained older hash with that name can
82
+ remain admissible.
83
+
84
+ `required_capabilities` maps a source namespace to a nonempty list of artifact
85
+ capability pins. Every listed pin is required, and each must be declared in
86
+ `capabilities.uses` or, for a local bare key, defined under
87
+ `capabilities.roles.capabilities`. Pins use the existing bare `<key>`,
88
+ `org/<key>`, or `app/<application-id>/<key>` grammar. Each namespace in either control must
89
+ also be reached by browser `data.read` or `data.write`. Preview bindings retain
90
+ the source namespace keys in these declarations.
91
+
92
+ The platform checks the persisted manifest against the authenticated user's
93
+ current artifact capability claims before ORM delegation. Caller-provided
94
+ claims and UI checks cannot satisfy the requirement. Namespace runtime grants,
95
+ operation capability checks, and row/write policies still apply independently;
96
+ these declarations grant no access. They constrain only Next.js browser ORM
97
+ requests, not server SDK calls or external ingress, which keep their own
98
+ declarations and authorization. They also do not transitively restrict ORM
99
+ work inside Managed Functions or general API calls; those surfaces enforce
100
+ their own permission contracts. The `createArtifactOrmExecutor` API is
101
+ unchanged.
102
+
103
+ For request correlation, the original `seq.api.fetch` response envelope includes
104
+ an `x-request-id` header after browser authorization succeeds, including later
105
+ denials. The platform generates this ID and records it in the browser admission
106
+ audit; it never uses an iframe-supplied ID. Authentication failures have no
107
+ browser admission ID. The ORM executor still returns the operation data, so
108
+ capture the fetch envelope when support tooling needs this correlation value.
109
+
43
110
  ### Exact numbers
44
111
 
45
112
  By default `numeric` and `bigint` values come back as JSON numbers, which
package/VERSION-PIN.md CHANGED
@@ -112,6 +112,28 @@ entry points, which remain classified **internal**. They add no package export
112
112
  subpath or supported direct HTTP surface. Publication and a reviewed public
113
113
  contract decision are separate from local build verification.
114
114
 
115
+ ## Planned browser ORM admission release
116
+
117
+ Artifact Studio 0.6.7 and Studio CLI 0.4.7 are the planned package pair for
118
+ schema-v2 Next.js browser `capabilities.data.operations` and
119
+ `capabilities.data.required_capabilities`. Atlas UI stays pinned at 3.4.1.
120
+ Publish the SDK and downstream CLI through the normal chain release, and
121
+ deploy the matching platform support before using these controls. This note
122
+ records the intended release; it is not evidence of npm publication or
123
+ environment rollout. The Next.js manifest/runtime surfaces remain internal.
124
+
125
+ Older schema-v2 parsers reject the new keys. Existing manifests that omit them
126
+ keep table-based browser admission. An empty `operations` list instead denies
127
+ all browser ORM operations; a nonempty list names exact persisted operations.
128
+ Namespace-scoped `required_capabilities` adds an all-of artifact capability
129
+ check to the existing namespace grants and row/write policies. These controls
130
+ do not change server SDK or external-ingress admission or transitively restrict
131
+ Managed Functions and general API calls. The executor API is unchanged.
132
+ Operation names retain active/previous persisted-operation fallback, so a
133
+ security-tightened query needs a fresh name to exclude retained older hashes;
134
+ autogenerated preview hashes never qualify. See the SDK README for the full
135
+ declaration contract and source-namespace preview behavior.
136
+
115
137
  ## What this means
116
138
 
117
139
  `shared/services/artifact-studio/src/build.ts` force-aliases platform peers via
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { type ArtifactStudioCustomRoles } from './manifest-custom-roles.js';
3
+ /** Additional browser admission rules; neither these rules nor data reach grant access. */
4
+ export declare const artifactNextBrowserDataSchema: z.ZodPrefault<z.ZodObject<{
5
+ operations: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ required_capabilities: z.ZodOptional<z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
7
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
8
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
9
+ }, z.core.$strict>>;
10
+ export type ArtifactNextBrowserData = z.infer<typeof artifactNextBrowserDataSchema>;
11
+ /** Restriction references must be backed by this browser's reach and declared artifact pins. */
12
+ export declare function validateBrowserDataAdmission({ data, uses, roles, ctx }: {
13
+ data: ArtifactNextBrowserData;
14
+ uses: readonly string[];
15
+ roles?: ArtifactStudioCustomRoles;
16
+ ctx: z.RefinementCtx;
17
+ }): void;
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ import { capabilityPinsSchema } from './manifest-custom-roles.js';
3
+ import { boundedRef, ormOperationRef, reachShape } from './manifest-next-reach.js';
4
+ const browserOperationRef = boundedRef.refine((ref) => ormOperationRef.test(ref), 'Browser ORM operations name each operation: namespace/ops/Operation');
5
+ /** Additional browser admission rules; neither these rules nor data reach grant access. */
6
+ export const artifactNextBrowserDataSchema = z.strictObject({
7
+ ...reachShape.data.unwrap().shape,
8
+ // Preserve absence: existing manifests use table-set admission. An explicit [] denies all.
9
+ operations: z.array(browserOperationRef).max(256).refine((operations) => new Set(operations).size === operations.length, 'Browser ORM operations must be unique').optional(),
10
+ // Keys name source namespaces, before development/preview namespace bindings are applied.
11
+ required_capabilities: z.preprocess((value, ctx) => {
12
+ // Zod records skip this own key before validating keys; reject it rather than silently drop it.
13
+ if (value !== null && typeof value === 'object' && Object.hasOwn(value, '__proto__')) {
14
+ ctx.addIssue({ code: 'custom', path: ['__proto__'], message: 'Capability guard keys are ORM source namespaces' });
15
+ }
16
+ return value;
17
+ }, z.record(z.string().regex(/^[a-z][a-z0-9_]{0,40}$/, 'Capability guard keys are ORM source namespaces'), capabilityPinsSchema.unwrap().min(1).refine((pins) => new Set(pins).size === pins.length, 'Required browser ORM capabilities must be unique'))).optional(),
18
+ }).prefault({});
19
+ /** Restriction references must be backed by this browser's reach and declared artifact pins. */
20
+ export function validateBrowserDataAdmission({ data, uses, roles, ctx }) {
21
+ const reached = new Set([...data.read, ...data.write].map((ref) => ref.split('/', 1)[0]));
22
+ for (const [index, operation] of (data.operations ?? []).entries()) {
23
+ const namespace = operation.split('/', 1)[0];
24
+ if (!reached.has(namespace)) {
25
+ ctx.addIssue({
26
+ code: 'custom', path: ['capabilities', 'data', 'operations', index],
27
+ message: `${operation} names a namespace that browser capabilities.data.read or write does not reach`,
28
+ });
29
+ }
30
+ }
31
+ const declared = new Set([...Object.keys(roles?.capabilities ?? {}), ...uses]);
32
+ for (const [namespace, pins] of Object.entries(data.required_capabilities ?? {})) {
33
+ const path = ['capabilities', 'data', 'required_capabilities', namespace];
34
+ if (!reached.has(namespace)) {
35
+ ctx.addIssue({ code: 'custom', path, message: `${namespace} is not reached by browser capabilities.data.read or write` });
36
+ }
37
+ for (const [index, pin] of pins.entries()) {
38
+ if (!declared.has(pin)) {
39
+ ctx.addIssue({
40
+ code: 'custom', path: [...path, index],
41
+ message: `Required capability "${pin}" must be declared in capabilities.roles.capabilities or capabilities.uses`,
42
+ });
43
+ }
44
+ }
45
+ }
46
+ }
@@ -22,6 +22,7 @@ export declare const ARTIFACT_NEXT_RESOURCE_DEFAULTS: {
22
22
  timeout_seconds: number;
23
23
  };
24
24
  export { artifactNextAuditCapabilitySchema };
25
+ export type { ArtifactNextBrowserData } from './manifest-next-browser-data.js';
25
26
  export * from './manifest-next-ingress.js';
26
27
  export declare const artifactNextServerCapabilitiesSchema: z.ZodPrefault<z.ZodObject<{
27
28
  audit: z.ZodOptional<z.ZodObject<{
@@ -228,6 +229,12 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
228
229
  next: z.ZodDefault<z.ZodLiteral<"16.3">>;
229
230
  }, z.core.$strict>>;
230
231
  capabilities: z.ZodPrefault<z.ZodObject<{
232
+ data: z.ZodPrefault<z.ZodObject<{
233
+ operations: z.ZodOptional<z.ZodArray<z.ZodString>>;
234
+ required_capabilities: z.ZodOptional<z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
235
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
236
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
237
+ }, z.core.$strict>>;
231
238
  allow_downloads: z.ZodDefault<z.ZodBoolean>;
232
239
  allow_popups: z.ZodDefault<z.ZodBoolean>;
233
240
  roles: z.ZodOptional<z.ZodObject<{
@@ -247,10 +254,6 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
247
254
  editor: "editor";
248
255
  admin: "admin";
249
256
  }>>>;
250
- data: z.ZodPrefault<z.ZodObject<{
251
- read: z.ZodDefault<z.ZodArray<z.ZodString>>;
252
- write: z.ZodDefault<z.ZodArray<z.ZodString>>;
253
- }, z.core.$strict>>;
254
257
  api: z.ZodPrefault<z.ZodObject<{
255
258
  read: z.ZodDefault<z.ZodArray<z.ZodString>>;
256
259
  write: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -1,6 +1,7 @@
1
1
  import { ontologyContractSchema } from '@sequenceholdings/orm/canonical';
2
2
  import { z } from 'zod';
3
3
  import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from './manifest-custom-roles.js';
4
+ import { artifactNextBrowserDataSchema, validateBrowserDataAdmission } from './manifest-next-browser-data.js';
4
5
  import { isManagedServerInvokePath } from './server/managed-server-path.js';
5
6
  import { artifactNextIngressSchema, validateIngressAgainstServer } from './manifest-next-ingress.js';
6
7
  import { artifactNextAuditCapabilitySchema, reachShape, serviceAccountRef } from './manifest-next-reach.js';
@@ -25,6 +26,7 @@ const strictCustomRoles = customRolesBlockSchema.safeExtend({
25
26
  }).strict();
26
27
  const browserCapabilities = z.strictObject({
27
28
  ...reachShape,
29
+ data: artifactNextBrowserDataSchema,
28
30
  allow_downloads: z.boolean().default(false),
29
31
  allow_popups: z.boolean().default(false),
30
32
  roles: strictCustomRoles.optional(),
@@ -125,6 +127,7 @@ export const artifactStudioNextManifestSchema = z.strictObject({
125
127
  sidebarIcon: z.string().max(64).optional(),
126
128
  sidebarBadge: sidebarBadgeSchema.strict().optional(),
127
129
  }).superRefine((manifest, ctx) => {
130
+ validateBrowserDataAdmission({ ...manifest.capabilities, ctx });
128
131
  for (const message of validateCapabilityPins({ uses: manifest.capabilities.uses, roles: manifest.capabilities.roles })) {
129
132
  ctx.addIssue({ code: 'custom', path: ['capabilities', 'uses'], message });
130
133
  }
@@ -112,6 +112,12 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
112
112
  next: z.ZodDefault<z.ZodLiteral<"16.3">>;
113
113
  }, z.core.$strict>>;
114
114
  capabilities: z.ZodPrefault<z.ZodObject<{
115
+ data: z.ZodPrefault<z.ZodObject<{
116
+ operations: z.ZodOptional<z.ZodArray<z.ZodString>>;
117
+ required_capabilities: z.ZodOptional<z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
118
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
119
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
120
+ }, z.core.$strict>>;
115
121
  allow_downloads: z.ZodDefault<z.ZodBoolean>;
116
122
  allow_popups: z.ZodDefault<z.ZodBoolean>;
117
123
  roles: z.ZodOptional<z.ZodObject<{
@@ -131,10 +137,6 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
131
137
  editor: "editor";
132
138
  admin: "admin";
133
139
  }>>>;
134
- data: z.ZodPrefault<z.ZodObject<{
135
- read: z.ZodDefault<z.ZodArray<z.ZodString>>;
136
- write: z.ZodDefault<z.ZodArray<z.ZodString>>;
137
- }, z.core.$strict>>;
138
140
  api: z.ZodPrefault<z.ZodObject<{
139
141
  read: z.ZodDefault<z.ZodArray<z.ZodString>>;
140
142
  write: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -1,5 +1,19 @@
1
1
  import { type ArtifactGatewayPublicKeys, type ArtifactRequestContext, type RequestContextV2External } from '../request-context.js';
2
2
  export type AnyArtifactRequestContext = ArtifactRequestContext | RequestContextV2External;
3
+ /**
4
+ * Optional human-directory prefetch for hosted admission, never an authorization result. The
5
+ * signed project is not yet bound to the credential's database record. After loading that record,
6
+ * the caller must still run its ordinary verifier with the runtime's keys and authoritative
7
+ * project, rechecking the clock after the await. Ineligible or invalid contexts return null so
8
+ * credential failures and external admission keep their existing checks and error ordering.
9
+ */
10
+ export declare function preverifyHostedBrowserContext({ token, publicKeys, environment, deploymentId, now }: {
11
+ token: string | null;
12
+ publicKeys: ArtifactGatewayPublicKeys;
13
+ environment: string;
14
+ deploymentId: string;
15
+ now?: number;
16
+ }): ArtifactRequestContext | null;
3
17
  /**
4
18
  * Version-dispatching verifier for platform code that must accept both Atlas-user (v1) and
5
19
  * external (v2) contexts, such as the broker. Applications never get this: it is not exported
@@ -1,7 +1,33 @@
1
- import { ArtifactContextError, requestContextV2ExternalSchema, } from '../request-context.js';
1
+ import { ArtifactContextError, ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX, artifactRequestContextSchema, requestContextV2ExternalSchema, } from '../request-context.js';
2
2
  import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX, verifySignedContext } from './signed-token.js';
3
3
  import { parseV1Context } from './v1-context.js';
4
4
  import { canonicalizeIngressPath, signedIngressPath } from '../ingress-routes.js';
5
+ /**
6
+ * Optional human-directory prefetch for hosted admission, never an authorization result. The
7
+ * signed project is not yet bound to the credential's database record. After loading that record,
8
+ * the caller must still run its ordinary verifier with the runtime's keys and authoritative
9
+ * project, rechecking the clock after the await. Ineligible or invalid contexts return null so
10
+ * credential failures and external admission keep their existing checks and error ordering.
11
+ */
12
+ export function preverifyHostedBrowserContext({ token, publicKeys, environment, deploymentId, now = Math.floor(Date.now() / 1000) }) {
13
+ try {
14
+ const { kid, payload } = verifySignedContext({ token, publicKeys });
15
+ if (kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX))
16
+ return null;
17
+ const parsed = artifactRequestContextSchema.safeParse(payload);
18
+ if (!parsed.success)
19
+ return null;
20
+ const context = parsed.data;
21
+ if (context.mode === 'development' || context.deploymentId !== deploymentId ||
22
+ context.sub.startsWith(ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX))
23
+ return null;
24
+ // Self-binding is safe only for this prefetch: ordinary admission binds the stored project.
25
+ return parseV1Context({ payload: context, projectId: context.projectId, environment, now });
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
5
31
  /**
6
32
  * Version-dispatching verifier for platform code that must accept both Atlas-user (v1) and
7
33
  * external (v2) contexts, such as the broker. Applications never get this: it is not exported
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
- "version": "0.6.6",
12
+ "version": "0.6.7",
13
13
  "description": "SDK types and CLI library for building Artifact Studio apps. Driven via `seq-studio artifact <sub>` (@sequenceholdings/studio-cli), which runs the `./cli` runCli export.",
14
14
  "type": "module",
15
15
  "exports": {
@@ -166,9 +166,9 @@
166
166
  "google-auth-library": "^10.5.0",
167
167
  "server-only": "0.0.1",
168
168
  "parse5": "7.3.0",
169
- "@sequenceholdings/lattice-form-renderer": "0.2.0",
169
+ "@sequenceholdings/storage": "0.1.1",
170
170
  "@sequenceholdings/orm": "2.5.0",
171
- "@sequenceholdings/storage": "0.1.1"
171
+ "@sequenceholdings/lattice-form-renderer": "0.2.0"
172
172
  },
173
173
  "devDependencies": {
174
174
  "@types/js-yaml": "^4.0.9",