@honua/react 0.1.4-beta.0 → 0.1.9-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contract/types.d.ts +1 -1
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +4 -0
- package/control-plane/admin-client.d.ts +87 -0
- package/control-plane/admin-client.d.ts.map +1 -0
- package/control-plane/admin-client.js +169 -0
- package/control-plane/client.d.ts.map +1 -1
- package/control-plane/client.js +21 -3
- package/control-plane/commands/catalog.d.ts +133 -0
- package/control-plane/commands/catalog.d.ts.map +1 -0
- package/control-plane/commands/catalog.js +353 -0
- package/control-plane/commands/errors.d.ts +96 -0
- package/control-plane/commands/errors.d.ts.map +1 -0
- package/control-plane/commands/errors.js +130 -0
- package/control-plane/commands/identity.d.ts +71 -0
- package/control-plane/commands/identity.d.ts.map +1 -0
- package/control-plane/commands/identity.js +0 -0
- package/control-plane/commands/index.d.ts +39 -0
- package/control-plane/commands/index.d.ts.map +1 -0
- package/control-plane/commands/index.js +34 -0
- package/control-plane/commands/runtime.d.ts +146 -0
- package/control-plane/commands/runtime.d.ts.map +1 -0
- package/control-plane/commands/runtime.js +318 -0
- package/control-plane/commands/types.d.ts +226 -0
- package/control-plane/commands/types.d.ts.map +1 -0
- package/control-plane/commands/types.js +30 -0
- package/control-plane/commands/validate.d.ts +27 -0
- package/control-plane/commands/validate.d.ts.map +1 -0
- package/control-plane/commands/validate.js +132 -0
- package/control-plane/generated/admin-api.d.ts +30263 -0
- package/control-plane/generated/admin-api.d.ts.map +1 -0
- package/control-plane/generated/admin-api.js +5 -0
- package/control-plane/generated/admin-operations.d.ts +4053 -0
- package/control-plane/generated/admin-operations.d.ts.map +1 -0
- package/control-plane/generated/admin-operations.js +820 -0
- package/control-plane/index.d.ts +11 -0
- package/control-plane/index.d.ts.map +1 -1
- package/control-plane/index.js +8 -0
- package/control-plane/types.d.ts +14 -0
- package/control-plane/types.d.ts.map +1 -1
- package/core/client.d.ts +6 -0
- package/core/client.d.ts.map +1 -1
- package/core/client.js +12 -10
- package/core/credential-redaction.d.ts +163 -0
- package/core/credential-redaction.d.ts.map +1 -0
- package/core/credential-redaction.js +389 -0
- package/core/error-classifications.d.ts +32 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +32 -0
- package/core/error-code-registry.d.ts +33 -1
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +32 -0
- package/core/errors.d.ts +1 -1
- package/core/errors.d.ts.map +1 -1
- package/core/geoparquet-sql.d.ts +1 -1
- package/core/geoparquet-sql.js +2 -2
- package/core/grpc-adapter.d.ts +2 -2
- package/core/grpc-adapter.d.ts.map +1 -1
- package/core/grpc-adapter.js +30 -6
- package/core/job-run-errors.d.ts +10 -0
- package/core/job-run-errors.d.ts.map +1 -0
- package/core/job-run-errors.js +14 -0
- package/core/job-run-lifecycle.d.ts +34 -0
- package/core/job-run-lifecycle.d.ts.map +1 -0
- package/core/job-run-lifecycle.js +193 -0
- package/core/ogc-processes.d.ts +38 -31
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +123 -173
- package/core/process-runner.js +15 -100
- package/core/stac.d.ts +2 -1
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +29 -11
- package/core/surfaces.d.ts +2 -8
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +21 -112
- package/core/types.d.ts +22 -8
- package/core/types.d.ts.map +1 -1
- package/gen/geospatial/v1/common_pb.d.ts +514 -0
- package/gen/geospatial/v1/common_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/common_pb.js +335 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts +1186 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/execution_types_pb.js +396 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts +403 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/feature_service_pb.js +47 -0
- package/gen/geospatial/v1/index.d.ts +5 -0
- package/gen/geospatial/v1/index.d.ts.map +1 -0
- package/gen/geospatial/v1/index.js +9 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts +285 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/spatial_types_pb.js +58 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts +379 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.js +151 -0
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/projection.js +10 -1
- package/interactions/declarative-contract.d.ts +149 -0
- package/interactions/declarative-contract.d.ts.map +1 -0
- package/interactions/declarative-contract.js +293 -0
- package/interactions/declarative.d.ts +198 -0
- package/interactions/declarative.d.ts.map +1 -0
- package/interactions/declarative.js +470 -0
- package/interactions/feature-state.d.ts +7 -0
- package/interactions/feature-state.d.ts.map +1 -1
- package/interactions/feature-state.js +10 -2
- package/map/maplibre-target.js +1 -1
- package/package.json +2 -2
- package/query-planner/grpc.d.ts +4 -4
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +11 -11
- package/query-planner/types.d.ts +4 -4
- package/query-planner/types.d.ts.map +1 -1
- package/realtime/sse.d.ts.map +1 -1
- package/realtime/sse.js +7 -1
- package/runtime/errors.d.ts +1 -1
- package/runtime/errors.d.ts.map +1 -1
- package/runtime/errors.js +2 -0
- package/runtime/generated/map-package-schema-meta.d.ts +11 -0
- package/runtime/generated/map-package-schema-meta.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-meta.js +12 -0
- package/runtime/generated/map-package-schema-validator.d.ts +16 -0
- package/runtime/generated/map-package-schema-validator.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-validator.js +1422 -0
- package/runtime/index.d.ts +5 -1
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +6 -0
- package/runtime/map-package-export.d.ts +151 -0
- package/runtime/map-package-export.d.ts.map +1 -0
- package/runtime/map-package-export.js +636 -0
- package/runtime/map-package-validation.d.ts +21 -1
- package/runtime/map-package-validation.d.ts.map +1 -1
- package/runtime/map-package-validation.js +132 -95
- package/runtime/map-package.d.ts +80 -0
- package/runtime/map-package.d.ts.map +1 -1
- package/runtime/style-compose.d.ts.map +1 -1
- package/runtime/style-compose.js +78 -9
- package/studio/chart-spec.d.ts +6 -6
- package/studio/chart-spec.d.ts.map +1 -1
- package/studio/chart-spec.js +6 -6
- package/studio/index.d.ts +17 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +15 -5
- package/studio/lifecycle-client.d.ts +262 -22
- package/studio/lifecycle-client.d.ts.map +1 -1
- package/studio/lifecycle-client.js +524 -24
- package/studio/lifecycle-errors.d.ts +8 -2
- package/studio/lifecycle-errors.d.ts.map +1 -1
- package/studio/lifecycle-errors.js +6 -1
- package/studio/lifecycle-types.d.ts +228 -8
- package/studio/lifecycle-types.d.ts.map +1 -1
- package/studio/publication-status.d.ts +55 -0
- package/studio/publication-status.d.ts.map +1 -0
- package/studio/publication-status.js +103 -0
- package/gen/honua/v1/feature_service_pb.d.ts +0 -918
- package/gen/honua/v1/feature_service_pb.d.ts.map +0 -1
- package/gen/honua/v1/feature_service_pb.js +0 -375
|
@@ -17,30 +17,38 @@
|
|
|
17
17
|
* `./lifecycle-errors.js` for the RFC 7807 problem-detail taxonomy and the
|
|
18
18
|
* `generation-conflict` discriminant.
|
|
19
19
|
*
|
|
20
|
-
* ##
|
|
20
|
+
* ## Publication proposals
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
22
|
+
* {@link HonuaStudioPublicationRequestsClient} submits an already-saved
|
|
23
|
+
* immutable version for publication, reads the resulting proposal, and walks
|
|
24
|
+
* it to a final state with a bounded, cancellable
|
|
25
|
+
* {@link HonuaStudioPublicationRequestsClient.poll}. Only `Active` yields a
|
|
26
|
+
* final publication URL; `Rejected` and `Failed` are terminal without one,
|
|
27
|
+
* and a status this release does not recognize is treated as neither.
|
|
28
|
+
* Approval is deliberately **not** part of this surface — see that class's
|
|
29
|
+
* doc comment.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
31
|
+
* ## Enumeration
|
|
32
|
+
*
|
|
33
|
+
* `contentItems.list()` (`GET /content-items`) and `drafts.list()`
|
|
34
|
+
* (`GET /package-drafts`) are the content-browser entry points added by
|
|
35
|
+
* `honua-server#3003`: filters (`family`, `workspaceId`, `owner`, `q`, plus
|
|
36
|
+
* `state` for content items), opaque keyset-cursor pagination ordered by
|
|
37
|
+
* `updatedAt` then row id descending, and — for content items — a joined
|
|
38
|
+
* Content Publication Registry lifecycle badge per row, so a lifecycle badge
|
|
39
|
+
* costs no extra request. Both resource groups also expose a bounded
|
|
40
|
+
* `listAll()` async generator and a `collect()` convenience that walk the
|
|
41
|
+
* cursor for you; neither can loop forever (see
|
|
42
|
+
* {@link HONUA_STUDIO_LIST_MAX_PAGES}).
|
|
43
|
+
*
|
|
44
|
+
* Cursors are **opaque**. Pass a `nextCursor` back verbatim; never build,
|
|
45
|
+
* parse, or persist-and-mutate one.
|
|
39
46
|
*
|
|
40
47
|
* @module
|
|
41
48
|
*/
|
|
42
|
-
import { isHonuaError } from "../core/errors.js";
|
|
49
|
+
import { HonuaTimeoutError, isHonuaError } from "../core/errors.js";
|
|
43
50
|
import { HonuaStudioError, classifyStudioProblemStatus } from "./lifecycle-errors.js";
|
|
51
|
+
import { isStudioPublicationActive, isStudioPublicationTerminal, normalizeStudioPublicationStatus, studioPublicationUrl, } from "./publication-status.js";
|
|
44
52
|
/** Default base path for the Studio package lifecycle API. */
|
|
45
53
|
export const HONUA_STUDIO_LIFECYCLE_BASE_PATH = "/api/v1/studio";
|
|
46
54
|
/** Construct a {@link HonuaStudioLifecycleClient} from an existing {@link HonuaClient}. */
|
|
@@ -57,6 +65,7 @@ export class HonuaStudioLifecycleClient {
|
|
|
57
65
|
#basePath;
|
|
58
66
|
packageFamilies;
|
|
59
67
|
drafts;
|
|
68
|
+
contentItems;
|
|
60
69
|
contentVersions;
|
|
61
70
|
publicationRequests;
|
|
62
71
|
rollbackRequests;
|
|
@@ -65,6 +74,7 @@ export class HonuaStudioLifecycleClient {
|
|
|
65
74
|
this.#basePath = normalizeBasePath(options.basePath ?? HONUA_STUDIO_LIFECYCLE_BASE_PATH);
|
|
66
75
|
this.packageFamilies = new HonuaStudioPackageFamiliesClient(this);
|
|
67
76
|
this.drafts = new HonuaStudioDraftsClient(this);
|
|
77
|
+
this.contentItems = new HonuaStudioContentItemsClient(this);
|
|
68
78
|
this.contentVersions = new HonuaStudioContentVersionsClient(this);
|
|
69
79
|
this.publicationRequests = new HonuaStudioPublicationRequestsClient(this);
|
|
70
80
|
this.rollbackRequests = new HonuaStudioRollbackRequestsClient(this);
|
|
@@ -72,7 +82,7 @@ export class HonuaStudioLifecycleClient {
|
|
|
72
82
|
get basePath() {
|
|
73
83
|
return this.#basePath;
|
|
74
84
|
}
|
|
75
|
-
/** Escape hatch for
|
|
85
|
+
/** Escape hatch for any endpoint that does not have a dedicated method on this client yet. */
|
|
76
86
|
raw(request) {
|
|
77
87
|
return this.request(request.method ?? "GET", request.path, request.body, {
|
|
78
88
|
signal: request.signal,
|
|
@@ -122,6 +132,35 @@ export class HonuaStudioDraftsClient {
|
|
|
122
132
|
create(request, options = {}) {
|
|
123
133
|
return this.#lifecycle.request("POST", "/package-drafts", request, options);
|
|
124
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* `GET /package-drafts` — one page of mutable drafts matching `filters`
|
|
137
|
+
* (`family`, `workspaceId`, `owner`, `q`), ordered by `updatedAt` then
|
|
138
|
+
* `draftId` descending.
|
|
139
|
+
*
|
|
140
|
+
* Rows are full {@link StudioPackageDraft} objects. Pass the response's
|
|
141
|
+
* `nextCursor` back as `filters.cursor` for the next page; its absence (or
|
|
142
|
+
* `null`) means this was the last one. Prefer {@link listAll} or
|
|
143
|
+
* {@link collect} over hand-rolling that loop.
|
|
144
|
+
*/
|
|
145
|
+
// `async` so the client-side `limit` guard in `withStudioListQuery` surfaces
|
|
146
|
+
// as a rejected promise, exactly like every server-side failure on this
|
|
147
|
+
// client, rather than as a synchronous throw callers would have to catch
|
|
148
|
+
// separately.
|
|
149
|
+
async list(filters = {}, options = {}) {
|
|
150
|
+
return await this.#lifecycle.request("GET", withStudioListQuery("/package-drafts", filters), undefined, options);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Walk `GET /package-drafts` to completion, yielding each page verbatim.
|
|
154
|
+
* Bounded by `options.maxPages` — see {@link HonuaStudioContentItemsClient.listAll}
|
|
155
|
+
* for the shared pagination rules.
|
|
156
|
+
*/
|
|
157
|
+
listAll(filters = {}, options = {}) {
|
|
158
|
+
return paginateStudioList((pageFilters, pageOptions) => this.list(pageFilters, pageOptions), filters, options);
|
|
159
|
+
}
|
|
160
|
+
/** Accumulate every draft page into one {@link StudioListCollection}. */
|
|
161
|
+
collect(filters = {}, options = {}) {
|
|
162
|
+
return collectStudioList(this.listAll(filters, options));
|
|
163
|
+
}
|
|
125
164
|
/** `GET /package-drafts/{draftId}` — retrieve a mutable draft. */
|
|
126
165
|
get(draftId, options = {}) {
|
|
127
166
|
return this.#lifecycle.request("GET", `/package-drafts/${encodeURIComponent(draftId)}`, undefined, options);
|
|
@@ -157,6 +196,185 @@ export class HonuaStudioDraftsClient {
|
|
|
157
196
|
return this.#lifecycle.request("POST", `/package-drafts/${encodeURIComponent(draftId)}/content-versions`, undefined, options);
|
|
158
197
|
}
|
|
159
198
|
}
|
|
199
|
+
/** Page size `GET /content-items` and `GET /package-drafts` use when `limit` is omitted. */
|
|
200
|
+
export const HONUA_STUDIO_LIST_DEFAULT_LIMIT = 25;
|
|
201
|
+
/**
|
|
202
|
+
* Largest `limit` the Studio list endpoints accept. The server clamps anything
|
|
203
|
+
* larger down to this value rather than erroring, which would silently hand a
|
|
204
|
+
* caller fewer rows than they asked for, so this client rejects an oversized
|
|
205
|
+
* `limit` up front instead.
|
|
206
|
+
*/
|
|
207
|
+
export const HONUA_STUDIO_LIST_MAX_LIMIT = 1_000;
|
|
208
|
+
/**
|
|
209
|
+
* Default hard bound on how many pages {@link HonuaStudioContentItemsClient.listAll}
|
|
210
|
+
* / {@link HonuaStudioDraftsClient.listAll} will fetch. There is no
|
|
211
|
+
* "walk forever" mode, and no option that produces one.
|
|
212
|
+
*/
|
|
213
|
+
export const HONUA_STUDIO_LIST_MAX_PAGES = 1_000;
|
|
214
|
+
/**
|
|
215
|
+
* True when a page is the last one — the server sends `nextCursor: null` on
|
|
216
|
+
* the final page, and its source-generated JSON context omits `null`
|
|
217
|
+
* properties, so "absent" and "null" mean the same thing. An empty-string
|
|
218
|
+
* cursor is treated as exhausted too rather than replayed as a filter value.
|
|
219
|
+
*/
|
|
220
|
+
export function isStudioListExhausted(page) {
|
|
221
|
+
return typeof page.nextCursor !== "string" || page.nextCursor.length === 0;
|
|
222
|
+
}
|
|
223
|
+
/** `/content-items` — enumerate Studio content items with joined publication badges. */
|
|
224
|
+
export class HonuaStudioContentItemsClient {
|
|
225
|
+
#lifecycle;
|
|
226
|
+
constructor(lifecycle) {
|
|
227
|
+
this.#lifecycle = lifecycle;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* `GET /content-items` — one page of content items matching `filters`
|
|
231
|
+
* (`family`, `workspaceId`, `owner`, `state`, `q`), ordered by `updatedAt`
|
|
232
|
+
* then `itemId` descending so pages stay stable while other rows are
|
|
233
|
+
* concurrently created or updated.
|
|
234
|
+
*
|
|
235
|
+
* Each row is a summary projection — no envelope — carrying the derived
|
|
236
|
+
* `state` (`draft`/`current`/`published`) and, when the Content Publication
|
|
237
|
+
* Registry has a publication whose `sourceContentId` is this `itemId`, a
|
|
238
|
+
* joined `publication` badge with the route's *own* lifecycle. That join is
|
|
239
|
+
* batched server-side, so a badge costs no extra request.
|
|
240
|
+
*
|
|
241
|
+
* A non-admin caller under `Studio:EndUserAuthorization:Enabled` always gets
|
|
242
|
+
* their own content regardless of the `owner` filter they pass, and a caller
|
|
243
|
+
* whose id cannot be resolved at all is refused with `403` rather than shown
|
|
244
|
+
* an unscoped list. An unknown `family` or `state` value is `400`.
|
|
245
|
+
*/
|
|
246
|
+
// `async` so the client-side `limit` guard in `withStudioListQuery` surfaces
|
|
247
|
+
// as a rejected promise, exactly like every server-side failure on this
|
|
248
|
+
// client, rather than as a synchronous throw callers would have to catch
|
|
249
|
+
// separately.
|
|
250
|
+
async list(filters = {}, options = {}) {
|
|
251
|
+
return await this.#lifecycle.request("GET", withStudioListQuery("/content-items", filters), undefined, options);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Walk `GET /content-items` to completion, yielding each page verbatim.
|
|
255
|
+
*
|
|
256
|
+
* - **Bounded by construction.** At most `options.maxPages` pages
|
|
257
|
+
* ({@link HONUA_STUDIO_LIST_MAX_PAGES} by default) are fetched. Reaching
|
|
258
|
+
* the cap ends the walk; the last yielded page still carries the
|
|
259
|
+
* `nextCursor` to resume from.
|
|
260
|
+
* - **Cursors stay opaque.** Each request replays the previous page's
|
|
261
|
+
* `nextCursor` verbatim; `filters.cursor` seeds the walk, so a walk can be
|
|
262
|
+
* resumed exactly where a previous one stopped.
|
|
263
|
+
* - **Cancellable.** `options.signal` aborts the in-flight page request and
|
|
264
|
+
* ends the iteration.
|
|
265
|
+
* - **A stuck server is not an infinite loop.** A page that repeats the
|
|
266
|
+
* cursor it was fetched with throws {@link HonuaStudioError} rather than
|
|
267
|
+
* paging forever.
|
|
268
|
+
* - **Standard error handling.** Every non-2xx surfaces as
|
|
269
|
+
* {@link HonuaStudioError} with its RFC 7807 problem details.
|
|
270
|
+
*/
|
|
271
|
+
listAll(filters = {}, options = {}) {
|
|
272
|
+
return paginateStudioList((pageFilters, pageOptions) => this.list(pageFilters, pageOptions), filters, options);
|
|
273
|
+
}
|
|
274
|
+
/** Accumulate every content-item page into one {@link StudioListCollection}. */
|
|
275
|
+
collect(filters = {}, options = {}) {
|
|
276
|
+
return collectStudioList(this.listAll(filters, options));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The shared bounded cursor walk behind every `listAll`. Generic over the row
|
|
281
|
+
* type so content items and drafts get identical pagination semantics from one
|
|
282
|
+
* implementation.
|
|
283
|
+
*/
|
|
284
|
+
async function* paginateStudioList(fetchPage, filters, options) {
|
|
285
|
+
const maxPages = normalizePageBound(options.maxPages);
|
|
286
|
+
const requestOptions = { signal: options.signal, headers: options.headers };
|
|
287
|
+
let cursor = filters.cursor;
|
|
288
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
289
|
+
throwIfAborted(options.signal);
|
|
290
|
+
const current = await fetchPage(cursor === undefined ? filters : { ...filters, cursor }, requestOptions);
|
|
291
|
+
yield current;
|
|
292
|
+
if (isStudioListExhausted(current))
|
|
293
|
+
return;
|
|
294
|
+
const next = current.nextCursor;
|
|
295
|
+
// A server that hands back the cursor it was just given would otherwise
|
|
296
|
+
// spin until `maxPages`, re-fetching the same page every time.
|
|
297
|
+
if (next === cursor) {
|
|
298
|
+
throw new HonuaStudioError("internal", 500, `Studio list pagination stalled: the server returned the same cursor it was queried with (${next}).`);
|
|
299
|
+
}
|
|
300
|
+
cursor = next;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** Drain a bounded page walk into one {@link StudioListCollection}. */
|
|
304
|
+
async function collectStudioList(pages) {
|
|
305
|
+
const items = [];
|
|
306
|
+
let pageCount = 0;
|
|
307
|
+
let total = 0;
|
|
308
|
+
let last;
|
|
309
|
+
for await (const page of pages) {
|
|
310
|
+
pageCount += 1;
|
|
311
|
+
total = page.total;
|
|
312
|
+
last = page;
|
|
313
|
+
items.push(...page.items);
|
|
314
|
+
}
|
|
315
|
+
// The generator stops either because the server said "no more" or because
|
|
316
|
+
// the page cap fired; only the second leaves a usable resume cursor behind.
|
|
317
|
+
const truncated = last !== undefined && !isStudioListExhausted(last);
|
|
318
|
+
return {
|
|
319
|
+
items,
|
|
320
|
+
pages: pageCount,
|
|
321
|
+
total,
|
|
322
|
+
nextCursor: truncated ? (last?.nextCursor ?? undefined) : undefined,
|
|
323
|
+
truncated,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function normalizePageBound(value) {
|
|
327
|
+
if (value === undefined)
|
|
328
|
+
return HONUA_STUDIO_LIST_MAX_PAGES;
|
|
329
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
330
|
+
throw new TypeError(`maxPages must be an integer >= 1, received ${String(value)}.`);
|
|
331
|
+
}
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Serialize the shared Studio list filters onto `path`.
|
|
336
|
+
*
|
|
337
|
+
* `family` and `state` accept one value or several and are sent in the
|
|
338
|
+
* server's comma-separated form. Blank strings are dropped rather than sent as
|
|
339
|
+
* an empty filter the server would trim to `null` anyway.
|
|
340
|
+
*/
|
|
341
|
+
function withStudioListQuery(path, filters) {
|
|
342
|
+
const params = new URLSearchParams();
|
|
343
|
+
const family = joinListFilter(filters.family);
|
|
344
|
+
if (family)
|
|
345
|
+
params.set("family", family);
|
|
346
|
+
const state = joinListFilter(filters.state);
|
|
347
|
+
if (state)
|
|
348
|
+
params.set("state", state);
|
|
349
|
+
if (filters.workspaceId?.trim())
|
|
350
|
+
params.set("workspaceId", filters.workspaceId);
|
|
351
|
+
if (filters.owner?.trim())
|
|
352
|
+
params.set("owner", filters.owner);
|
|
353
|
+
if (filters.q?.trim())
|
|
354
|
+
params.set("q", filters.q);
|
|
355
|
+
if (filters.cursor)
|
|
356
|
+
params.set("cursor", filters.cursor);
|
|
357
|
+
if (filters.limit !== undefined)
|
|
358
|
+
params.set("limit", String(normalizeListLimit(filters.limit)));
|
|
359
|
+
const query = params.toString();
|
|
360
|
+
return query ? `${path}?${query}` : path;
|
|
361
|
+
}
|
|
362
|
+
function joinListFilter(value) {
|
|
363
|
+
const values = (typeof value === "string" ? [value] : (value ?? [])).map((entry) => entry.trim()).filter(Boolean);
|
|
364
|
+
return values.length > 0 ? values.join(",") : undefined;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Reject a `limit` the server would not honor as asked. Below `1` it would be
|
|
368
|
+
* replaced with the default and above {@link HONUA_STUDIO_LIST_MAX_LIMIT} it
|
|
369
|
+
* would be silently clamped — both hand back a page size the caller did not
|
|
370
|
+
* request, which is worse than a thrown programming error.
|
|
371
|
+
*/
|
|
372
|
+
function normalizeListLimit(limit) {
|
|
373
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > HONUA_STUDIO_LIST_MAX_LIMIT) {
|
|
374
|
+
throw new TypeError(`limit must be an integer between 1 and ${HONUA_STUDIO_LIST_MAX_LIMIT}, received ${String(limit)}.`);
|
|
375
|
+
}
|
|
376
|
+
return limit;
|
|
377
|
+
}
|
|
160
378
|
/** `/content-items/{itemId}/versions*` and `/version-comparisons` — immutable content versions. */
|
|
161
379
|
export class HonuaStudioContentVersionsClient {
|
|
162
380
|
#lifecycle;
|
|
@@ -187,7 +405,58 @@ export class HonuaStudioContentVersionsClient {
|
|
|
187
405
|
return this.#lifecycle.request("POST", `/content-items/${encodeURIComponent(itemId)}/versions/${encodeURIComponent(versionId)}/reopen`, request, options);
|
|
188
406
|
}
|
|
189
407
|
}
|
|
190
|
-
/**
|
|
408
|
+
/** Default delay between {@link HonuaStudioPublicationRequestsClient.poll} attempts. */
|
|
409
|
+
export const HONUA_STUDIO_PUBLICATION_POLL_INTERVAL_MS = 1_000;
|
|
410
|
+
/** Default attempt bound for {@link HonuaStudioPublicationRequestsClient.poll}. */
|
|
411
|
+
export const HONUA_STUDIO_PUBLICATION_POLL_MAX_ATTEMPTS = 30;
|
|
412
|
+
/**
|
|
413
|
+
* Body keys a publication *submission* may never carry. Approval is a
|
|
414
|
+
* separate principal acting through the canonical Admin API/CLI, so the SDK
|
|
415
|
+
* refuses to serialize a submission that tries to assert its own decision,
|
|
416
|
+
* override policy, or seed a terminal status — see
|
|
417
|
+
* {@link HonuaStudioPublicationRequestsClient} for why this is enforced
|
|
418
|
+
* client-side as well as server-side.
|
|
419
|
+
*
|
|
420
|
+
* Matching is on the exact key (case-insensitively); an additive server field
|
|
421
|
+
* that merely *mentions* approval (`approvalPolicyId`, say) is unaffected.
|
|
422
|
+
*/
|
|
423
|
+
const REJECTED_SUBMISSION_KEYS = new Set([
|
|
424
|
+
"status",
|
|
425
|
+
"state",
|
|
426
|
+
"approve",
|
|
427
|
+
"approved",
|
|
428
|
+
"approval",
|
|
429
|
+
"approvedat",
|
|
430
|
+
"approvedby",
|
|
431
|
+
"autoapprove",
|
|
432
|
+
"selfapprove",
|
|
433
|
+
"skipapproval",
|
|
434
|
+
"bypassapproval",
|
|
435
|
+
"bypasspolicy",
|
|
436
|
+
"policyoverride",
|
|
437
|
+
"overridepolicy",
|
|
438
|
+
"force",
|
|
439
|
+
]);
|
|
440
|
+
/**
|
|
441
|
+
* `/content-items/{itemId}/versions/{versionId}/publish-requests` — submit an
|
|
442
|
+
* already-saved immutable version for publication and walk the resulting
|
|
443
|
+
* proposal to a final state.
|
|
444
|
+
*
|
|
445
|
+
* ## Approval is a different principal
|
|
446
|
+
*
|
|
447
|
+
* This client can **create**, **get** and **poll** a publication proposal. It
|
|
448
|
+
* deliberately exposes no approve, authorize, force, or policy-override
|
|
449
|
+
* capability, and {@link create} refuses to serialize a submission body that
|
|
450
|
+
* tries to smuggle one (see {@link REJECTED_SUBMISSION_KEYS}). A proposer
|
|
451
|
+
* therefore cannot approve their own publication, or bypass policy, through
|
|
452
|
+
* any method on this class — approval happens through the canonical Admin
|
|
453
|
+
* API/CLI acting as a separate principal, and the server enforces the same
|
|
454
|
+
* rule with a `403` (`code: "forbidden"`) if a proposer tries anyway.
|
|
455
|
+
*
|
|
456
|
+
* {@link HonuaStudioLifecycleClient.raw} is a generic HTTP escape hatch with
|
|
457
|
+
* no Studio approval semantics of its own; it reaches whatever the server
|
|
458
|
+
* exposes and is subject to exactly the same server-side authorization.
|
|
459
|
+
*/
|
|
191
460
|
export class HonuaStudioPublicationRequestsClient {
|
|
192
461
|
#lifecycle;
|
|
193
462
|
constructor(lifecycle) {
|
|
@@ -195,12 +464,243 @@ export class HonuaStudioPublicationRequestsClient {
|
|
|
195
464
|
}
|
|
196
465
|
/**
|
|
197
466
|
* `POST /content-items/{itemId}/versions/{versionId}/publish-requests` —
|
|
198
|
-
*
|
|
199
|
-
*
|
|
467
|
+
* submit an already-saved immutable version for publication.
|
|
468
|
+
*
|
|
469
|
+
* The submission carries only the identity of what is being published
|
|
470
|
+
* (`itemId`, `versionId`, the optional `contentHash` pin), the requested
|
|
471
|
+
* route/visibility `intent`, and an optional `idempotencyKey` so a replayed
|
|
472
|
+
* submit resolves to the same proposal. It never carries a decision:
|
|
473
|
+
* an approval, policy-override, or `status` key in `request` throws a
|
|
474
|
+
* {@link HonuaStudioError} with `code: "validation"` **before** anything is
|
|
475
|
+
* sent.
|
|
476
|
+
*
|
|
477
|
+
* The response carries the joined `operationInstanceId`, `proposalId`,
|
|
478
|
+
* `proposalUri`, `auditId` and `correlationId` identifiers verbatim.
|
|
479
|
+
*/
|
|
480
|
+
// `async` so the client-side approval guard below surfaces as a rejected
|
|
481
|
+
// promise, exactly like every server-side failure on this client, rather
|
|
482
|
+
// than as a synchronous throw callers would have to catch separately.
|
|
483
|
+
async create(itemId, versionId, request = {}, options = {}) {
|
|
484
|
+
assertNoApprovalFields(request);
|
|
485
|
+
return this.#lifecycle.request("POST", `${this.#path(itemId, versionId)}/publish-requests`, request, options);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* `GET /content-items/{itemId}/versions/{versionId}/publish-requests/{requestId}`
|
|
489
|
+
* — read one publication proposal's current status and joined identifiers.
|
|
490
|
+
*
|
|
491
|
+
* A proposal that does not exist, or is not reachable from this
|
|
492
|
+
* `itemId`/`versionId` pair, throws `code: "not-found"`; one outside the
|
|
493
|
+
* caller's owner/tenant scope throws `code: "forbidden"`.
|
|
200
494
|
*/
|
|
201
|
-
|
|
202
|
-
return this.#lifecycle.request("
|
|
495
|
+
get(itemId, versionId, requestId, options = {}) {
|
|
496
|
+
return this.#lifecycle.request("GET", `${this.#path(itemId, versionId)}/publish-requests/${encodeURIComponent(requestId)}`, undefined, options);
|
|
203
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Poll one publication proposal until it reaches a terminal state or a
|
|
500
|
+
* bound is reached, whichever comes first.
|
|
501
|
+
*
|
|
502
|
+
* - **Bounded by construction.** Every poll stops after at most
|
|
503
|
+
* `maxAttempts` `GET`s (and `timeoutMs`, when given). There is no
|
|
504
|
+
* unbounded mode.
|
|
505
|
+
* - **A final publication URL only at `Active`.** `Rejected` and `Failed`
|
|
506
|
+
* end the poll with `terminal: true`, `active: false` and no URL.
|
|
507
|
+
* - **An unrecognized status is never final.** A state this release does
|
|
508
|
+
* not know keeps the poll waiting until a bound is hit, and comes back as
|
|
509
|
+
* `state: undefined, terminal: false, active: false`.
|
|
510
|
+
* - **Cancellable.** `options.signal` aborts the in-flight request and the
|
|
511
|
+
* wait between attempts; the returned promise rejects with the signal's
|
|
512
|
+
* abort reason — including when the abort lands mid-request, where the
|
|
513
|
+
* underlying `HonuaClient` would otherwise normalize the rejection to a
|
|
514
|
+
* generic SDK abort error.
|
|
515
|
+
* - **`timeoutMs` is a real wall-clock bound.** It aborts an in-flight `GET`
|
|
516
|
+
* as well as being checked between attempts, and the inter-attempt wait is
|
|
517
|
+
* clamped to the time left, so a hung response cannot leave the poll
|
|
518
|
+
* pending past the deadline even when the underlying `HonuaClient` has no
|
|
519
|
+
* `timeoutMs` of its own. A deadline reached with at least one observation
|
|
520
|
+
* returns `exhausted: "timeout"`; a deadline that expires before the very
|
|
521
|
+
* first response arrives has nothing to report and throws
|
|
522
|
+
* {@link HonuaTimeoutError}.
|
|
523
|
+
* - **Standard error handling.** Every non-2xx surfaces as
|
|
524
|
+
* {@link HonuaStudioError} with its RFC 7807 problem details, exactly as
|
|
525
|
+
* for every other method on this client.
|
|
526
|
+
*/
|
|
527
|
+
async poll(itemId, versionId, requestId, options = {}) {
|
|
528
|
+
const maxAttempts = normalizePollBound(options.maxAttempts, HONUA_STUDIO_PUBLICATION_POLL_MAX_ATTEMPTS);
|
|
529
|
+
const intervalMs = normalizeInterval(options.intervalMs);
|
|
530
|
+
const timeoutMs = options.timeoutMs === undefined ? undefined : normalizeInterval(options.timeoutMs);
|
|
531
|
+
// The wall-clock bound owns its own abort signal rather than being a
|
|
532
|
+
// between-attempts `Date.now()` check alone: `HonuaClient` carries no
|
|
533
|
+
// request timeout unless the consumer configured one, so a `GET` that is
|
|
534
|
+
// accepted and never answered would otherwise hold the poll open forever
|
|
535
|
+
// in spite of a finite `timeoutMs`.
|
|
536
|
+
const deadline = createPollDeadline(timeoutMs, options.signal);
|
|
537
|
+
const requestOptions = { signal: deadline.signal, headers: options.headers };
|
|
538
|
+
let observed;
|
|
539
|
+
try {
|
|
540
|
+
// Bounded by construction: the loop counter is the only thing that drives
|
|
541
|
+
// it, `maxAttempts` is validated to be a finite integer >= 1, and every
|
|
542
|
+
// path out of the body either returns or advances the counter.
|
|
543
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
544
|
+
throwIfAborted(options.signal);
|
|
545
|
+
let request;
|
|
546
|
+
try {
|
|
547
|
+
request = await this.get(itemId, versionId, requestId, requestOptions);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
// The caller's cancellation is reported with the caller's own
|
|
551
|
+
// `reason`. `HonuaClient.pipelineFetch` normalizes an in-flight fetch
|
|
552
|
+
// abort into a generic SDK abort error, which would otherwise lose it.
|
|
553
|
+
throwIfAborted(options.signal);
|
|
554
|
+
if (deadline.expired())
|
|
555
|
+
return this.#timedOut(observed, attempt - 1, timeoutMs);
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
const state = normalizeStudioPublicationStatus(request.status);
|
|
559
|
+
observed = { request, state };
|
|
560
|
+
options.onStatus?.(request, state);
|
|
561
|
+
if (isStudioPublicationTerminal(request.status)) {
|
|
562
|
+
return toPollOutcome(request, state, attempt, undefined);
|
|
563
|
+
}
|
|
564
|
+
if (attempt >= maxAttempts) {
|
|
565
|
+
return toPollOutcome(request, state, attempt, "max-attempts");
|
|
566
|
+
}
|
|
567
|
+
const remainingMs = deadline.remainingMs();
|
|
568
|
+
if (remainingMs !== undefined && remainingMs <= 0) {
|
|
569
|
+
return toPollOutcome(request, state, attempt, "timeout");
|
|
570
|
+
}
|
|
571
|
+
// Never sleep past the deadline: a `intervalMs` larger than the time
|
|
572
|
+
// left would overshoot the documented bound by a whole interval.
|
|
573
|
+
await delay(remainingMs === undefined ? intervalMs : Math.min(intervalMs, remainingMs), options.signal);
|
|
574
|
+
const leftAfterWait = deadline.remainingMs();
|
|
575
|
+
if (leftAfterWait !== undefined && leftAfterWait <= 0) {
|
|
576
|
+
return toPollOutcome(request, state, attempt, "timeout");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
deadline.dispose();
|
|
582
|
+
}
|
|
583
|
+
// Unreachable while `maxAttempts >= 1`; kept so the bound is enforced by
|
|
584
|
+
// the type system rather than by a comment.
|
|
585
|
+
throw new TypeError(`maxAttempts must be an integer >= 1, received ${String(maxAttempts)}.`);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* The wall-clock bound fired. With at least one observation the poll reports
|
|
589
|
+
* it the documented way (`exhausted: "timeout"`); with none there is no
|
|
590
|
+
* proposal to report, so the deadline surfaces as the SDK's own
|
|
591
|
+
* {@link HonuaTimeoutError} rather than an invented empty outcome.
|
|
592
|
+
*/
|
|
593
|
+
#timedOut(observed, attempts, timeoutMs) {
|
|
594
|
+
if (!observed)
|
|
595
|
+
throw new HonuaTimeoutError(timeoutMs ?? 0);
|
|
596
|
+
return toPollOutcome(observed.request, observed.state, attempts, "timeout");
|
|
597
|
+
}
|
|
598
|
+
#path(itemId, versionId) {
|
|
599
|
+
return `/content-items/${encodeURIComponent(itemId)}/versions/${encodeURIComponent(versionId)}`;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function createPollDeadline(timeoutMs, callerSignal) {
|
|
603
|
+
if (timeoutMs === undefined) {
|
|
604
|
+
return {
|
|
605
|
+
signal: callerSignal,
|
|
606
|
+
expired: () => false,
|
|
607
|
+
remainingMs: () => undefined,
|
|
608
|
+
dispose: () => undefined,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const controller = new AbortController();
|
|
612
|
+
const expiresAt = Date.now() + timeoutMs;
|
|
613
|
+
let expired = false;
|
|
614
|
+
const timer = setTimeout(() => {
|
|
615
|
+
expired = true;
|
|
616
|
+
controller.abort();
|
|
617
|
+
}, timeoutMs);
|
|
618
|
+
const onCallerAbort = () => {
|
|
619
|
+
controller.abort(callerSignal ? abortReason(callerSignal) : undefined);
|
|
620
|
+
};
|
|
621
|
+
if (callerSignal?.aborted)
|
|
622
|
+
onCallerAbort();
|
|
623
|
+
else
|
|
624
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
625
|
+
return {
|
|
626
|
+
signal: controller.signal,
|
|
627
|
+
expired: () => expired,
|
|
628
|
+
remainingMs: () => expiresAt - Date.now(),
|
|
629
|
+
dispose: () => {
|
|
630
|
+
clearTimeout(timer);
|
|
631
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function toPollOutcome(request, state, attempts, exhausted) {
|
|
636
|
+
return {
|
|
637
|
+
request,
|
|
638
|
+
state,
|
|
639
|
+
terminal: isStudioPublicationTerminal(request.status),
|
|
640
|
+
active: isStudioPublicationActive(request.status),
|
|
641
|
+
publicationUrl: studioPublicationUrl(request),
|
|
642
|
+
attempts,
|
|
643
|
+
exhausted,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Throw a client-side `validation` {@link HonuaStudioError} — nothing is sent
|
|
648
|
+
* — when a submission body carries an approval or policy-override key.
|
|
649
|
+
*/
|
|
650
|
+
function assertNoApprovalFields(request) {
|
|
651
|
+
const offending = Object.keys(request).filter((key) => REJECTED_SUBMISSION_KEYS.has(key.toLowerCase()));
|
|
652
|
+
if (offending.length === 0)
|
|
653
|
+
return;
|
|
654
|
+
const rule = [
|
|
655
|
+
"the proposer cannot approve their own publication or override policy.",
|
|
656
|
+
"Approval is a separate principal acting through the Admin API/CLI.",
|
|
657
|
+
].join(" ");
|
|
658
|
+
throw new HonuaStudioError("validation", 400, `A publication submission must not carry ${offending.join(", ")}: ${rule}`);
|
|
659
|
+
}
|
|
660
|
+
function normalizePollBound(value, fallback) {
|
|
661
|
+
if (value === undefined)
|
|
662
|
+
return fallback;
|
|
663
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
664
|
+
throw new TypeError(`maxAttempts must be an integer >= 1, received ${String(value)}.`);
|
|
665
|
+
}
|
|
666
|
+
return value;
|
|
667
|
+
}
|
|
668
|
+
function normalizeInterval(value) {
|
|
669
|
+
if (value === undefined)
|
|
670
|
+
return HONUA_STUDIO_PUBLICATION_POLL_INTERVAL_MS;
|
|
671
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
672
|
+
throw new TypeError(`Poll interval and timeout must be finite and >= 0, received ${String(value)}.`);
|
|
673
|
+
}
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
function throwIfAborted(signal) {
|
|
677
|
+
if (signal?.aborted)
|
|
678
|
+
throw abortReason(signal);
|
|
679
|
+
}
|
|
680
|
+
function abortReason(signal) {
|
|
681
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
682
|
+
}
|
|
683
|
+
/** `setTimeout` that settles early — by rejecting with the abort reason — when `signal` aborts. */
|
|
684
|
+
function delay(ms, signal) {
|
|
685
|
+
if (ms <= 0) {
|
|
686
|
+
throwIfAborted(signal);
|
|
687
|
+
return Promise.resolve();
|
|
688
|
+
}
|
|
689
|
+
return new Promise((resolve, reject) => {
|
|
690
|
+
if (signal?.aborted) {
|
|
691
|
+
reject(abortReason(signal));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const timer = setTimeout(() => {
|
|
695
|
+
signal?.removeEventListener("abort", onAbort);
|
|
696
|
+
resolve();
|
|
697
|
+
}, ms);
|
|
698
|
+
const onAbort = () => {
|
|
699
|
+
clearTimeout(timer);
|
|
700
|
+
reject(abortReason(signal));
|
|
701
|
+
};
|
|
702
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
703
|
+
});
|
|
204
704
|
}
|
|
205
705
|
/** `/content-items/{itemId}/rollback-requests`. */
|
|
206
706
|
export class HonuaStudioRollbackRequestsClient {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every non-2xx response from the Studio package lifecycle API
|
|
5
5
|
* (`/api/v{version}/studio`) is either a handler-generated client error
|
|
6
|
-
* (`400`/`404`/`409`) or a caught internal failure (`500`), and
|
|
6
|
+
* (`400`/`401`/`403`/`404`/`409`) or a caught internal failure (`500`), and
|
|
7
|
+
* both use
|
|
7
8
|
* RFC 7807 problem details with `type: "https://honua.io/problems/studio"`
|
|
8
9
|
* (see `docs/internal/admin-api/studio-package-lifecycle.md` in
|
|
9
10
|
* `honua-server`). {@link HonuaStudioError} normalizes both into one typed
|
|
@@ -30,6 +31,11 @@
|
|
|
30
31
|
* status code of the failed response:
|
|
31
32
|
*
|
|
32
33
|
* - `validation` — `400`, request or envelope failed validation.
|
|
34
|
+
* - `unauthorized` — `401`, the request carried no usable principal.
|
|
35
|
+
* - `forbidden` — `403`, the principal is authenticated but is not permitted
|
|
36
|
+
* to perform the operation — the owner/tenant scope ceiling, and the
|
|
37
|
+
* separate-approver rule that stops a proposer from approving their own
|
|
38
|
+
* publication (see {@link HonuaStudioPublicationRequestsClient}).
|
|
33
39
|
* - `not-found` — `404`, the draft/version/content item does not exist (or,
|
|
34
40
|
* per the API doc, is not reachable from the given `itemId`/`versionId`
|
|
35
41
|
* pair — cross-item ids are treated as not found).
|
|
@@ -41,7 +47,7 @@
|
|
|
41
47
|
*
|
|
42
48
|
* @experimental
|
|
43
49
|
*/
|
|
44
|
-
export type HonuaStudioProblemKind = "validation" | "not-found" | "generation-conflict" | "internal" | "unknown";
|
|
50
|
+
export type HonuaStudioProblemKind = "validation" | "unauthorized" | "forbidden" | "not-found" | "generation-conflict" | "internal" | "unknown";
|
|
45
51
|
/**
|
|
46
52
|
* RFC 7807 problem-details body returned by the Studio package lifecycle API.
|
|
47
53
|
* `type` is `"https://honua.io/problems/studio"` on every documented error
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-errors.d.ts","sourceRoot":"","sources":["../../../src/studio/lifecycle-errors.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"lifecycle-errors.d.ts","sourceRoot":"","sources":["../../../src/studio/lifecycle-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,sBAAsB,GAC9B,YAAY,GACZ,cAAc,GACd,WAAW,GACX,WAAW,GACX,qBAAqB,GACrB,UAAU,GACV,SAAS,CAAC;AAEd;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IACpD,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACnC;AAED,8FAA8F;AAC9F,eAAO,MAAM,yBAAyB,EAAG,kCAA2C,CAAC;AAErF;;;;;;;GAOG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,SAAgB,IAAI,EAAE,sBAAsB,CAAC;IAC7C,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,OAAO,EAAE,yBAAyB,GAAG,SAAS,CAAC;gBAG7D,IAAI,EAAE,sBAAsB,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,yBAAyB,EACnC,OAAO,GAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAQ7C;AAED,+CAA+C;AAC/C,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAEzF;AAED,oFAAoF;AACpF,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,sBAAsB,CAiBtF"}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every non-2xx response from the Studio package lifecycle API
|
|
5
5
|
* (`/api/v{version}/studio`) is either a handler-generated client error
|
|
6
|
-
* (`400`/`404`/`409`) or a caught internal failure (`500`), and
|
|
6
|
+
* (`400`/`401`/`403`/`404`/`409`) or a caught internal failure (`500`), and
|
|
7
|
+
* both use
|
|
7
8
|
* RFC 7807 problem details with `type: "https://honua.io/problems/studio"`
|
|
8
9
|
* (see `docs/internal/admin-api/studio-package-lifecycle.md` in
|
|
9
10
|
* `honua-server`). {@link HonuaStudioError} normalizes both into one typed
|
|
@@ -65,6 +66,10 @@ export function classifyStudioProblemStatus(statusCode) {
|
|
|
65
66
|
switch (statusCode) {
|
|
66
67
|
case 400:
|
|
67
68
|
return "validation";
|
|
69
|
+
case 401:
|
|
70
|
+
return "unauthorized";
|
|
71
|
+
case 403:
|
|
72
|
+
return "forbidden";
|
|
68
73
|
case 404:
|
|
69
74
|
return "not-found";
|
|
70
75
|
case 409:
|