@volter/twin-sentry 1.0.0 → 1.0.1

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 (44) hide show
  1. package/README.md +3 -2
  2. package/api/spec/SOURCE.md +19 -2
  3. package/api/spec/openapi.json.gz +0 -0
  4. package/api/src/generated/ui.gen.json +1 -1
  5. package/api/src/manifest.ts +4 -3
  6. package/api/src/semantics/api.ts +3 -2
  7. package/api/src/semantics/doors.ts +45 -19
  8. package/api/src/semantics/shared.ts +1 -5
  9. package/dist/api/spec/SOURCE.md +19 -2
  10. package/dist/api/spec/openapi.json.gz +0 -0
  11. package/dist/api/src/generated/ui.gen.json +1 -1
  12. package/dist/api/src/manifest.js +4 -3
  13. package/dist/api/src/manifest.ts +4 -3
  14. package/dist/api/src/semantics/api.js +4 -2
  15. package/dist/api/src/semantics/api.ts +3 -2
  16. package/dist/api/src/semantics/doors.d.ts +1 -4
  17. package/dist/api/src/semantics/doors.js +51 -19
  18. package/dist/api/src/semantics/doors.ts +45 -19
  19. package/dist/api/src/semantics/shared.d.ts +0 -1
  20. package/dist/api/src/semantics/shared.js +1 -2
  21. package/dist/api/src/semantics/shared.ts +1 -5
  22. package/dist/ingest/src/manifest.js +3 -0
  23. package/dist/ingest/src/manifest.ts +3 -0
  24. package/dist/journeys/customer-life.json +11 -13
  25. package/dist/journeys/customer-life.outline.md +5 -5
  26. package/dist/journeys/demand.json +1 -45
  27. package/dist/journeys/first-use.json +22 -0
  28. package/dist/src/cli.js +1 -1
  29. package/dist/src/index.js +2 -1
  30. package/dist/src/manifest.js +3 -2
  31. package/dist/src/semantics/shared.d.ts +5 -4
  32. package/dist/src/semantics/shared.js +22 -26
  33. package/generated/pack-facts.json +14 -0
  34. package/ingest/src/manifest.ts +3 -0
  35. package/journeys/customer-life.json +11 -13
  36. package/journeys/customer-life.outline.md +5 -5
  37. package/journeys/demand.json +1 -45
  38. package/journeys/first-use.json +22 -0
  39. package/package.json +10 -9
  40. package/src/cli.ts +1 -1
  41. package/src/index.ts +2 -1
  42. package/src/manifest.ts +3 -2
  43. package/src/semantics/shared.ts +22 -26
  44. package/dist/generated/pack-facts.json +0 -87
package/README.md CHANGED
@@ -9,8 +9,9 @@ The API and ingest share one vendor store.
9
9
  | `sentry.io`, `us.sentry.io`, `de.sentry.io` | sentry-cli release creation, finalization, commits and previous-release lookup, repository listing and source-map chunk/artifact uploads; account project creation and client-key reads; declared vendor refresh reads. | [api/spec](./api/spec/SOURCE.md), Sentry's pinned OpenAPI document and evidenced patches |
10
10
 
11
11
  The application's SDK authenticates ingestion with its public DSN key. API calls use scoped personal tokens or
12
- organization CI tokens, stored by their hash. The credential door is bootstrap signup: it creates the account,
13
- first token, project and client key named by the environment, returning the same credentials at every boot.
12
+ organization CI tokens, stored by their hash. The credential door retains the selected organization and each
13
+ deployment's project, client key and scoped personal token. It returns the same DSN and token on later issuance,
14
+ with the API's native bearer authorization and origin.
14
15
  Additional projects are created through `createOrganizationProject`; their DSNs are read through `listProjectKeys`.
15
16
 
16
17
  World account doors:
@@ -2,7 +2,7 @@
2
2
 
3
3
  - **Why this document:** Sentry's own OpenAPI document for its web API, as it publishes it for its API reference:
4
4
  getsentry/sentry-api-schema at `98ec014705fbecf18c2650097e4630afe51ffd85` (2026-09-29), `openapi-derefed.json`,
5
- vendored gzipped and unchanged (`openapi.json.gz`).
5
+ vendored gzipped (`openapi.json.gz`); packaged public sample changes are listed below.
6
6
  - **Servers:** `https://{region}.sentry.io` (us, de); sentry-cli and the applications call `https://sentry.io/api/0/`.
7
7
  - **Corrections** (`patches.json`, each with its evidence): the chunk upload (`/chunk-upload/`) and artifact bundle assembly
8
8
  (`/artifactbundle/assemble/`) that sentry-cli uploads source maps through; and a single project's release paths
@@ -11,5 +11,22 @@
11
11
 
12
12
  - **Resource normalization:** the five refresh reads reference named copies of their unchanged inline response schemas. The patches cite each operation in the pinned upstream document; no wire field is added or removed.
13
13
 
14
- - **Vendored document SHA-256** (openapi.json.gz, decompressed bytes where gzipped): `bb07d2bb747b10b31c14722066cd1d11bd0bb76eea8dcfb5f341b4c421c1c1f9`.
14
+ - **Original upstream document SHA-256** (openapi.json.gz, decompressed bytes where gzipped): `bb07d2bb747b10b31c14722066cd1d11bd0bb76eea8dcfb5f341b4c421c1c1f9`.
15
15
  - **Read by:** Codex contributor.
16
+
17
+ ## Packaged public example scalars
18
+
19
+ `api/spec/openapi.json.gz` retains the pinned source above with only the string-scalar replacements listed below. Original inputs are retained outside the package at `/Volumes/PeakSSD/peak-floor/state/prod-ops/releases/vo3818-source-captures/sentry/api/spec/openapi.json.gz` and `/Volumes/PeakSSD/peak-floor/state/prod-ops/releases/vo3818-source-captures/sentry/api/spec/openapi.json`. Original uncompressed SHA-256: `bb07d2bb747b10b31c14722066cd1d11bd0bb76eea8dcfb5f341b4c421c1c1f9`; packaged uncompressed SHA-256: `e39f236da03e6162bc1fcee602a0865b04f7642fd00eb362cb256bd1cfb01d80`. Every byte outside the listed JSON string values is unchanged. These public samples are inert; runtime credentials are issued through the existing doors.
20
+
21
+ | JSON pointer | Packaged value | Reason |
22
+ | --- | --- | --- |
23
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1latest-base~1/get/responses/200/content/application~1json/examples/LatestBaseSnapshot/value/images/0/image_url` | `/api/0/projects/org-slug/my-project/files/images/000000000000/` | Preserves the image key component of the public example URL. |
24
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1latest-base~1/get/responses/200/content/application~1json/examples/LatestBaseSnapshot/value/images/0/key` | `000000000000` | Inert fixed-width image identifier; matching image URL uses the same identifier. |
25
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1{snapshot_id}~1/get/responses/200/content/application~1json/examples/SnapshotWithComparison(Diff)/value/changed/0/head_image/key` | `000000000000` | Inert fixed-width image identifier; matching image URL uses the same identifier. |
26
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1{snapshot_id}~1/get/responses/200/content/application~1json/examples/SnapshotWithoutComparison(Solo)/value/images/0/key` | `000000000000` | Inert fixed-width image identifier; matching image URL uses the same identifier. |
27
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1{snapshot_id}~1images~1{image_identifier}~1/get/responses/200/content/application~1json/examples/ChangedImage/value/head_image/image_url` | `/api/0/projects/org-slug/project-slug/files/images/000000000000/` | Preserves the image key component of the public example URL. |
28
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1{snapshot_id}~1images~1{image_identifier}~1/get/responses/200/content/application~1json/examples/ChangedImage/value/head_image/key` | `000000000000` | Inert fixed-width image identifier; matching image URL uses the same identifier. |
29
+ | `/paths/~1api~10~1projects~1{organization_id_or_slug}~1{project_id_or_slug}~1hooks~1/get/responses/200/content/application~1json/example/0/secret` | `0000000000000000000000000000000000000000000000000000000000000000` | Inert fixed-width webhook secret sample, shared across hook read/create/update examples. |
30
+ | `/paths/~1api~10~1projects~1{organization_id_or_slug}~1{project_id_or_slug}~1hooks~1/post/responses/201/content/application~1json/example/secret` | `0000000000000000000000000000000000000000000000000000000000000000` | Inert fixed-width webhook secret sample, shared across hook read/create/update examples. |
31
+ | `/paths/~1api~10~1projects~1{organization_id_or_slug}~1{project_id_or_slug}~1hooks~1{hook_id}~1/get/responses/200/content/application~1json/example/secret` | `0000000000000000000000000000000000000000000000000000000000000000` | Inert fixed-width webhook secret sample, shared across hook read/create/update examples. |
32
+ | `/paths/~1api~10~1projects~1{organization_id_or_slug}~1{project_id_or_slug}~1hooks~1{hook_id}~1/put/responses/200/content/application~1json/example/secret` | `0000000000000000000000000000000000000000000000000000000000000000` | Inert fixed-width webhook secret sample, shared across hook read/create/update examples. |
Binary file
@@ -1 +1 @@
1
- {"generatedBy":"scripts/derive-pack.ts","resources":{"organization":{"actions":[],"states":{},"embeds":{}},"project":{"list":"/api/0/organizations/{organization_id_or_slug}/projects/","actions":[],"states":{},"embeds":{}},"project_key":{"actions":[],"states":{},"embeds":{}},"event":{"retrieve":"/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/events/{event_id}/","actions":[],"states":{},"embeds":{}},"issue":{"retrieve":"/api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/","actions":[],"states":{},"embeds":{}},"release":{"actions":[],"states":{},"embeds":{}},"monitor":{"list":"/api/0/organizations/{organization_id_or_slug}/monitors/","actions":[],"states":{},"embeds":{}},"check_in":{"list":"/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/monitors/{monitor_id_or_slug}/checkins/","actions":[],"states":{"status":["error","in_progress","ok"]},"embeds":{}},"_seq_token":{"actions":[],"states":{},"embeds":{}},"_seq_event":{"actions":[],"states":{},"embeds":{}},"_seq_envelope":{"actions":[],"states":{},"embeds":{}}}}
1
+ {"generatedBy":"scripts/derive-pack.ts","resources":{"organization":{"actions":[],"states":{},"embeds":{}},"project":{"list":"/api/0/organizations/{organization_id_or_slug}/projects/","actions":[],"states":{},"embeds":{}},"project_key":{"actions":[],"states":{},"embeds":{}},"event":{"retrieve":"/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/events/{event_id}/","actions":[],"states":{},"embeds":{}},"issue":{"retrieve":"/api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/","actions":[],"states":{},"embeds":{}},"release":{"actions":[],"states":{},"embeds":{}},"monitor":{"list":"/api/0/organizations/{organization_id_or_slug}/monitors/","actions":[],"states":{},"embeds":{}},"check_in":{"list":"/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/monitors/{monitor_id_or_slug}/checkins/","actions":[],"states":{"status":["error","in_progress","ok"]},"embeds":{}},"_seq_token":{"actions":[],"states":{},"embeds":{}},"_seq_event":{"actions":[],"states":{},"embeds":{}},"_seq_envelope":{"actions":[],"states":{},"embeds":{}},"_seq_project_key":{"actions":[],"states":{},"embeds":{}}}}
@@ -8,6 +8,7 @@ export const manifest: DerivedManifest = {
8
8
  // both lanes are one service, so one store: the ingest lane writes events the API lane reads, and the doors' accounts
9
9
  // are the ingest lane's too (aws's lanes likewise)
10
10
  service: 'sentry',
11
+ hosts: [{ host: 'sentry.io' }, { host: 'us.sentry.io' }, { host: 'de.sentry.io' }],
11
12
  body: { json: 'always' },
12
13
  ids: { template: '{uuid}' },
13
14
  time: 'iso',
@@ -25,10 +26,10 @@ export const manifest: DerivedManifest = {
25
26
  // source: https://docs.sentry.io/api/pagination/ "Pagination in the API is handled via the Link header standard:"
26
27
  resources: {
27
28
  // source: https://docs.sentry.io/api/projects/create-a-new-project/ "6758470122493650"
28
- organization: { idPrefix: '', alternateKeys: ['slug'], ids: '{digits:16}', refresh: { list: 'listOrganizations', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
29
+ organization: { idPrefix: '', unique: [{ fields: ['slug'], refusal: { status: 409, message: 'An organization with this slug already exists.' } }], alternateKeys: ['slug'], ids: '{digits:16}', refresh: { list: 'listOrganizations', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
29
30
  // source: spec:listOrganizationProjects "Return a list of projects bound to a organization"
30
31
  // source: https://docs.sentry.io/api/projects/create-a-new-project/ "6758470122493650"
31
- project: { idPrefix: '', alternateKeys: ['slug'], ids: '{digits:16}', parent: { param: 'organization_id_or_slug', field: '_organization', resource: 'organization' }, refresh: { list: 'listOrganizationProjects', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
32
+ project: { idPrefix: '', unique: [{ fields: ['_organization', 'slug'], refusal: { status: 409, message: 'A project with this slug already exists.' } }], alternateKeys: ['slug'], ids: '{digits:16}', parent: { param: 'organization_id_or_slug', field: '_organization', resource: 'organization' }, refresh: { list: 'listOrganizationProjects', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
32
33
  project_key: { idPrefix: '', ids: '{hex:32}', refresh: { none: 'Client keys contain credential secrets. Credential custody keeps their secret-bearing reads out of refresh.' } },
33
34
  // source: spec:listProjectEvents "Return a list of events bound to a project."
34
35
  event: { idPrefix: '', ids: '{hex:32}', key: '{project_id_or_slug}:{event_id}', vendorUnderscored: ['_meta'], ...states.event,
@@ -43,7 +44,7 @@ export const manifest: DerivedManifest = {
43
44
  // source: spec:listProjectMonitorCheckins "Retrieve Check-Ins for a Monitor by Project"
44
45
  // source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/monitors/serializers.py "str(obj.guid)"
45
46
  check_in: { ...states.check_in, idPrefix: '', ids: '{uuid}', parent: { params: ['organization_id_or_slug', 'project_id_or_slug', 'monitor_id_or_slug'], value: '{monitor_id_or_slug}', field: '_monitor', resource: 'monitor', where: { _organization: '{organization_id_or_slug}', 'project.id': '{project_id_or_slug}', id: '{monitor_id_or_slug}' } }, refresh: { list: 'listProjectMonitorCheckins', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
46
- _seq_token: { idPrefix: '', ids: '{n}' }, _seq_event: { idPrefix: '', ids: '{n}' }, _seq_envelope: { idPrefix: '', ids: '{n}' },
47
+ _seq_token: { idPrefix: '', ids: '{n}' }, _seq_event: { idPrefix: '', ids: '{n}' }, _seq_envelope: { idPrefix: '', ids: '{n}' }, _seq_project_key: { idPrefix: '', ids: '{n}' },
47
48
  },
48
49
  unmodeled: [
49
50
  'getOrganization', 'listProjectEvents', 'getProjectRelease', 'getProjectMonitor',
@@ -1,5 +1,5 @@
1
1
  import { gunzip, type HandlerContext } from '@volter/world-core';
2
- import { bearerToken, detail, EVENT, forbidden, grants, keyOf, makeProject, notFound, orgFor, PROJECT, projectFor, projectOf, projectView, type Row, wire } from './shared.ts';
2
+ import { bearerToken, detail, EVENT, forbidden, grants, keyOf, makeProject, notFound, orgFor, PROJECT, projectFor, projectOf, type Row, wire } from './shared.ts';
3
3
 
4
4
  // Releases, as sentry-cli makes, finalizes and associates commits with them (`releases new`, `releases finalize`,
5
5
  // `releases set-commits`), for one project at its path or for the organization; and the organization's repositories the
@@ -199,8 +199,9 @@ export async function createOrganizationProject(ctx: HandlerContext): Promise<Re
199
199
  if (!/^(?![0-9]+$)[a-z0-9_-]+$/.test(slug)) return Response.json({ slug: ['Enter a valid slug.'] }, { status: 400 });
200
200
  if (projectOf(ctx, org, slug)) return Response.json({ slug: ['A project with this slug already exists.'] }, { status: 409 });
201
201
  const project = await makeProject(ctx, org, slug, typeof b.platform === 'string' ? b.platform : null);
202
+ if (project instanceof Response) return project;
202
203
  await ctx.write(PROJECT, String(project.id), { name: b.name }, 'project.update');
203
- return Response.json(projectView(ctx, ctx.row(PROJECT, String(project.id))!, org), { status: 201 });
204
+ return Response.json(wire(ctx, ctx.row(PROJECT, String(project.id))!), { status: 201 });
204
205
  }
205
206
 
206
207
  // source: spec:listProjectKeys "Return a list of client keys bound to a project."
@@ -8,31 +8,61 @@
8
8
  // an organization auth token (Settings > Auth Tokens), or with kind
9
9
  // personal a personal token with its scopes, shown once
10
10
  import type { HandlerContext } from '@volter/world-core';
11
- import { dsnOf, heldOrgToken, keyOf, makeOrg, makeOrgToken, makePersonalToken, makeProject, orgBySlug, TOKEN } from './shared.ts';
11
+ import { CLIENT_KEY, dsnOf, keyOf, makeClientKey, makeOrg, makeOrgToken, makePersonalToken, makeProject, orgBySlug, ORG, PROJECT, readToken, TOKEN } from './shared.ts';
12
12
 
13
13
  const bad = (error: string): Response => Response.json({ error }, { status: 400 });
14
14
  const bodyOf = (ctx: HandlerContext): Record<string, unknown> => (ctx.body && typeof ctx.body === 'object' ? ctx.body : {}) as Record<string, unknown>;
15
15
  const SLUG = /^[a-z0-9][a-z0-9_-]{0,49}$/;
16
16
 
17
- /** Bootstrap (RECIPE, State): the account's sign-up creates its first credential and env-named project/key.
18
- * The World's application: the organization `world`, its project `app`, the project's DSN and a token of the
19
- * organization's, made the first time and answered the same at every boot: the token is re-derived from its secret and
20
- * answered while it matches its hash; one revoked is not made again. */
17
+ /** The deployment's project client key and an independently issued auth token in its organization. */
21
18
  export async function appCredentials(ctx: HandlerContext): Promise<Response> {
22
- const b = bodyOf(ctx);
23
- const org = await makeOrg(ctx, String(b.organization ?? 'world'), String(b.name ?? 'World'));
24
- const project = await makeProject(ctx, org, String(b.project ?? 'app'), String(b.platform ?? 'javascript'));
25
- const key = keyOf(ctx, project)!;
26
- const revoked = ctx.rowsRaw(TOKEN, { withDeleted: true }).some((t) => t.deleted === true && t.organization === org.id && t.name === 'world-app');
27
- if (revoked) return Response.json({ error: 'the application token was revoked' }, { status: 409 });
28
- const token = (await heldOrgToken(ctx, org, 'world-app')) ?? await makePersonalToken(ctx, org, 'world-app', ['org:read', 'project:read', 'project:write', 'project:releases', 'event:read', 'alerts:read']);
29
- return Response.json({ dsn: dsnOf(org, project, key), auth_token: token, org: org.slug, org_id: org.id, project: project.slug }, { status: 201 });
19
+ try {
20
+ const b = bodyOf(ctx);
21
+ const organizationId = await ctx.account();
22
+ const org = await ctx.tenant(ORG, async (organization) => {
23
+ const label = typeof b.organization === 'string' ? b.organization : organization.id === 'default' ? 'world' : organization.id;
24
+ const rule = { max: 50, lower: true, allowed: /[a-z0-9_-]/, first: /[a-z0-9]/ };
25
+ const name = typeof b.name === 'string' ? b.name : organization.label;
26
+ let made = await makeOrg(ctx, ctx.vendorName(label, rule, { identity: organization.id }), name, organization.members[0]!);
27
+ if (made instanceof Response) made = await makeOrg(ctx, ctx.vendorName(label, rule, { suffixed: true, identity: organization.id }), name, organization.members[0]!);
28
+ if (made instanceof Response) throw await duplicate(made, organization.id);
29
+ return String(made.id);
30
+ });
31
+ const project = await ctx.deploymentObject(`project:${String(org.id)}`, PROJECT, async (deployment) => {
32
+ const label = typeof b.project === 'string' ? b.project : deployment || 'app';
33
+ const identity = deployment === '' ? organizationId : `${organizationId}/${deployment}`;
34
+ const rule = { max: Number.MAX_SAFE_INTEGER, lower: true, allowed: /[a-z0-9_-]/, first: /[a-z_-]/ };
35
+ const platform = typeof b.platform === 'string' ? b.platform : 'javascript';
36
+ let made = await makeProject(ctx, org, ctx.vendorName(label, rule, { identity }), platform);
37
+ if (made instanceof Response) made = await makeProject(ctx, org, ctx.vendorName(label, rule, { suffixed: true, identity }), platform);
38
+ if (made instanceof Response) throw await duplicate(made, identity);
39
+ return String(made.id);
40
+ });
41
+ const key = await ctx.accountKey('client-key', CLIENT_KEY, {
42
+ make: async () => String((keyOf(ctx, project) ?? await makeClientKey(ctx, org, project)).id),
43
+ read: async (row) => row._project === project.id && row.isActive !== false && typeof row.public === 'string' ? row.public : undefined,
44
+ });
45
+ const token = await ctx.accountKey('auth-token', TOKEN, {
46
+ make: async () => ctx.crypto.sha256(await makePersonalToken(ctx, org, ctx.deployment() || 'world-app', ['org:read', 'project:read', 'project:write', 'project:releases', 'event:read', 'alerts:read'])),
47
+ read: (row) => readToken(ctx, org, row),
48
+ });
49
+ return Response.json({ dsn: dsnOf(org, project, { public: key.secret }), auth_token: token.secret, authorization: `Bearer ${token.secret}`, url: 'https://sentry.io', org: org.slug, org_id: org.id, project: project.slug }, { status: 201 });
50
+ } catch (error) {
51
+ if (error instanceof Response) return error;
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ async function duplicate(response: Response, identity: string): Promise<Response> {
57
+ const body = await response.json() as { slug: string[] };
58
+ return Response.json({ slug: body.slug.map((message) => `${message} (${identity})`) }, { status: response.status });
30
59
  }
31
60
 
32
61
  export async function organizations(ctx: HandlerContext): Promise<Response> {
33
62
  const b = bodyOf(ctx);
34
63
  if (typeof b.slug !== 'string' || !SLUG.test(b.slug)) return bad('a slug (lowercase letters, digits, - and _) is required');
35
- const org = await makeOrg(ctx, b.slug, typeof b.name === 'string' ? b.name : b.slug);
64
+ const org = await makeOrg(ctx, b.slug, typeof b.name === 'string' ? b.name : b.slug, (await ctx.organization()).members[0]!);
65
+ if (org instanceof Response) return org;
36
66
  return Response.json({ id: org.id, slug: org.slug }, { status: 201 });
37
67
  }
38
68
 
@@ -42,9 +72,5 @@ export async function authTokens(ctx: HandlerContext): Promise<Response> {
42
72
  if (!org) return bad('organization must name an organization the World holds');
43
73
  if (typeof b.name !== 'string' || !b.name) return bad('a name is required');
44
74
  const scopes = Array.isArray(b.scopes) ? b.scopes.map(String) : ['org:read', 'project:read', 'event:read'];
45
- return b.kind === 'personal' ? Response.json({ token: await makePersonalToken(ctx, org, String(b.name), scopes) }, { status: 201 }) : issueOrganizationToken(ctx, org, String(b.name));
46
- }
47
-
48
- async function issueOrganizationToken(ctx: HandlerContext, org: Record<string, unknown>, name: string): Promise<Response> {
49
- return Response.json({ token: await makeOrgToken(ctx, org, name) }, { status: 201 });
75
+ return b.kind === 'personal' ? Response.json({ token: await makePersonalToken(ctx, org, String(b.name), scopes) }, { status: 201 }) : Response.json({ token: await makeOrgToken(ctx, org, String(b.name)) }, { status: 201 });
50
76
  }
@@ -1,7 +1,7 @@
1
1
  // What every family of Sentry's API shares: who is calling (an auth token the World issued), the organization a request
2
2
  // names and whether the token may act on it, and the API's answers (Django REST framework's `{"detail": ...}`).
3
3
  import type { HandlerContext } from '@volter/world-core';
4
- import { bearerToken, grants, orgBySlug, projectOf, type Row, wire } from '../../../src/semantics/shared.ts';
4
+ import { bearerToken, grants, orgBySlug, projectOf, type Row } from '../../../src/semantics/shared.ts';
5
5
 
6
6
  export * from '../../../src/semantics/shared.ts';
7
7
 
@@ -29,7 +29,3 @@ export function projectFor(ctx: HandlerContext, scope: string): { org: Row; proj
29
29
  const project = projectOf(ctx, org, String(ctx.call.params.project_id_or_slug ?? ''));
30
30
  return project ? { org, project } : notFound();
31
31
  }
32
-
33
-
34
-
35
- export function projectView(ctx: HandlerContext, p: Row, _org: Row): Row { return wire(ctx, p); }
@@ -2,7 +2,7 @@
2
2
 
3
3
  - **Why this document:** Sentry's own OpenAPI document for its web API, as it publishes it for its API reference:
4
4
  getsentry/sentry-api-schema at `98ec014705fbecf18c2650097e4630afe51ffd85` (2026-09-29), `openapi-derefed.json`,
5
- vendored gzipped and unchanged (`openapi.json.gz`).
5
+ vendored gzipped (`openapi.json.gz`); packaged public sample changes are listed below.
6
6
  - **Servers:** `https://{region}.sentry.io` (us, de); sentry-cli and the applications call `https://sentry.io/api/0/`.
7
7
  - **Corrections** (`patches.json`, each with its evidence): the chunk upload (`/chunk-upload/`) and artifact bundle assembly
8
8
  (`/artifactbundle/assemble/`) that sentry-cli uploads source maps through; and a single project's release paths
@@ -11,5 +11,22 @@
11
11
 
12
12
  - **Resource normalization:** the five refresh reads reference named copies of their unchanged inline response schemas. The patches cite each operation in the pinned upstream document; no wire field is added or removed.
13
13
 
14
- - **Vendored document SHA-256** (openapi.json.gz, decompressed bytes where gzipped): `bb07d2bb747b10b31c14722066cd1d11bd0bb76eea8dcfb5f341b4c421c1c1f9`.
14
+ - **Original upstream document SHA-256** (openapi.json.gz, decompressed bytes where gzipped): `bb07d2bb747b10b31c14722066cd1d11bd0bb76eea8dcfb5f341b4c421c1c1f9`.
15
15
  - **Read by:** Codex contributor.
16
+
17
+ ## Packaged public example scalars
18
+
19
+ `api/spec/openapi.json.gz` retains the pinned source above with only the string-scalar replacements listed below. Original inputs are retained outside the package at `/Volumes/PeakSSD/peak-floor/state/prod-ops/releases/vo3818-source-captures/sentry/api/spec/openapi.json.gz` and `/Volumes/PeakSSD/peak-floor/state/prod-ops/releases/vo3818-source-captures/sentry/api/spec/openapi.json`. Original uncompressed SHA-256: `bb07d2bb747b10b31c14722066cd1d11bd0bb76eea8dcfb5f341b4c421c1c1f9`; packaged uncompressed SHA-256: `e39f236da03e6162bc1fcee602a0865b04f7642fd00eb362cb256bd1cfb01d80`. Every byte outside the listed JSON string values is unchanged. These public samples are inert; runtime credentials are issued through the existing doors.
20
+
21
+ | JSON pointer | Packaged value | Reason |
22
+ | --- | --- | --- |
23
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1latest-base~1/get/responses/200/content/application~1json/examples/LatestBaseSnapshot/value/images/0/image_url` | `/api/0/projects/org-slug/my-project/files/images/000000000000/` | Preserves the image key component of the public example URL. |
24
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1latest-base~1/get/responses/200/content/application~1json/examples/LatestBaseSnapshot/value/images/0/key` | `000000000000` | Inert fixed-width image identifier; matching image URL uses the same identifier. |
25
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1{snapshot_id}~1/get/responses/200/content/application~1json/examples/SnapshotWithComparison(Diff)/value/changed/0/head_image/key` | `000000000000` | Inert fixed-width image identifier; matching image URL uses the same identifier. |
26
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1{snapshot_id}~1/get/responses/200/content/application~1json/examples/SnapshotWithoutComparison(Solo)/value/images/0/key` | `000000000000` | Inert fixed-width image identifier; matching image URL uses the same identifier. |
27
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1{snapshot_id}~1images~1{image_identifier}~1/get/responses/200/content/application~1json/examples/ChangedImage/value/head_image/image_url` | `/api/0/projects/org-slug/project-slug/files/images/000000000000/` | Preserves the image key component of the public example URL. |
28
+ | `/paths/~1api~10~1organizations~1{organization_id_or_slug}~1preprodartifacts~1snapshots~1{snapshot_id}~1images~1{image_identifier}~1/get/responses/200/content/application~1json/examples/ChangedImage/value/head_image/key` | `000000000000` | Inert fixed-width image identifier; matching image URL uses the same identifier. |
29
+ | `/paths/~1api~10~1projects~1{organization_id_or_slug}~1{project_id_or_slug}~1hooks~1/get/responses/200/content/application~1json/example/0/secret` | `0000000000000000000000000000000000000000000000000000000000000000` | Inert fixed-width webhook secret sample, shared across hook read/create/update examples. |
30
+ | `/paths/~1api~10~1projects~1{organization_id_or_slug}~1{project_id_or_slug}~1hooks~1/post/responses/201/content/application~1json/example/secret` | `0000000000000000000000000000000000000000000000000000000000000000` | Inert fixed-width webhook secret sample, shared across hook read/create/update examples. |
31
+ | `/paths/~1api~10~1projects~1{organization_id_or_slug}~1{project_id_or_slug}~1hooks~1{hook_id}~1/get/responses/200/content/application~1json/example/secret` | `0000000000000000000000000000000000000000000000000000000000000000` | Inert fixed-width webhook secret sample, shared across hook read/create/update examples. |
32
+ | `/paths/~1api~10~1projects~1{organization_id_or_slug}~1{project_id_or_slug}~1hooks~1{hook_id}~1/put/responses/200/content/application~1json/example/secret` | `0000000000000000000000000000000000000000000000000000000000000000` | Inert fixed-width webhook secret sample, shared across hook read/create/update examples. |
Binary file
@@ -1 +1 @@
1
- {"generatedBy":"scripts/derive-pack.ts","resources":{"organization":{"actions":[],"states":{},"embeds":{}},"project":{"list":"/api/0/organizations/{organization_id_or_slug}/projects/","actions":[],"states":{},"embeds":{}},"project_key":{"actions":[],"states":{},"embeds":{}},"event":{"retrieve":"/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/events/{event_id}/","actions":[],"states":{},"embeds":{}},"issue":{"retrieve":"/api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/","actions":[],"states":{},"embeds":{}},"release":{"actions":[],"states":{},"embeds":{}},"monitor":{"list":"/api/0/organizations/{organization_id_or_slug}/monitors/","actions":[],"states":{},"embeds":{}},"check_in":{"list":"/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/monitors/{monitor_id_or_slug}/checkins/","actions":[],"states":{"status":["error","in_progress","ok"]},"embeds":{}},"_seq_token":{"actions":[],"states":{},"embeds":{}},"_seq_event":{"actions":[],"states":{},"embeds":{}},"_seq_envelope":{"actions":[],"states":{},"embeds":{}}}}
1
+ {"generatedBy":"scripts/derive-pack.ts","resources":{"organization":{"actions":[],"states":{},"embeds":{}},"project":{"list":"/api/0/organizations/{organization_id_or_slug}/projects/","actions":[],"states":{},"embeds":{}},"project_key":{"actions":[],"states":{},"embeds":{}},"event":{"retrieve":"/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/events/{event_id}/","actions":[],"states":{},"embeds":{}},"issue":{"retrieve":"/api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/","actions":[],"states":{},"embeds":{}},"release":{"actions":[],"states":{},"embeds":{}},"monitor":{"list":"/api/0/organizations/{organization_id_or_slug}/monitors/","actions":[],"states":{},"embeds":{}},"check_in":{"list":"/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/monitors/{monitor_id_or_slug}/checkins/","actions":[],"states":{"status":["error","in_progress","ok"]},"embeds":{}},"_seq_token":{"actions":[],"states":{},"embeds":{}},"_seq_event":{"actions":[],"states":{},"embeds":{}},"_seq_envelope":{"actions":[],"states":{},"embeds":{}},"_seq_project_key":{"actions":[],"states":{},"embeds":{}}}}
@@ -6,6 +6,7 @@ export const manifest = {
6
6
  // both lanes are one service, so one store: the ingest lane writes events the API lane reads, and the doors' accounts
7
7
  // are the ingest lane's too (aws's lanes likewise)
8
8
  service: 'sentry',
9
+ hosts: [{ host: 'sentry.io' }, { host: 'us.sentry.io' }, { host: 'de.sentry.io' }],
9
10
  body: { json: 'always' },
10
11
  ids: { template: '{uuid}' },
11
12
  time: 'iso',
@@ -23,10 +24,10 @@ export const manifest = {
23
24
  // source: https://docs.sentry.io/api/pagination/ "Pagination in the API is handled via the Link header standard:"
24
25
  resources: {
25
26
  // source: https://docs.sentry.io/api/projects/create-a-new-project/ "6758470122493650"
26
- organization: { idPrefix: '', alternateKeys: ['slug'], ids: '{digits:16}', refresh: { list: 'listOrganizations', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
27
+ organization: { idPrefix: '', unique: [{ fields: ['slug'], refusal: { status: 409, message: 'An organization with this slug already exists.' } }], alternateKeys: ['slug'], ids: '{digits:16}', refresh: { list: 'listOrganizations', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
27
28
  // source: spec:listOrganizationProjects "Return a list of projects bound to a organization"
28
29
  // source: https://docs.sentry.io/api/projects/create-a-new-project/ "6758470122493650"
29
- project: { idPrefix: '', alternateKeys: ['slug'], ids: '{digits:16}', parent: { param: 'organization_id_or_slug', field: '_organization', resource: 'organization' }, refresh: { list: 'listOrganizationProjects', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
30
+ project: { idPrefix: '', unique: [{ fields: ['_organization', 'slug'], refusal: { status: 409, message: 'A project with this slug already exists.' } }], alternateKeys: ['slug'], ids: '{digits:16}', parent: { param: 'organization_id_or_slug', field: '_organization', resource: 'organization' }, refresh: { list: 'listOrganizationProjects', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
30
31
  project_key: { idPrefix: '', ids: '{hex:32}', refresh: { none: 'Client keys contain credential secrets. Credential custody keeps their secret-bearing reads out of refresh.' } },
31
32
  // source: spec:listProjectEvents "Return a list of events bound to a project."
32
33
  event: { idPrefix: '', ids: '{hex:32}', key: '{project_id_or_slug}:{event_id}', vendorUnderscored: ['_meta'], ...states.event,
@@ -41,7 +42,7 @@ export const manifest = {
41
42
  // source: spec:listProjectMonitorCheckins "Retrieve Check-Ins for a Monitor by Project"
42
43
  // source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/monitors/serializers.py "str(obj.guid)"
43
44
  check_in: { ...states.check_in, idPrefix: '', ids: '{uuid}', parent: { params: ['organization_id_or_slug', 'project_id_or_slug', 'monitor_id_or_slug'], value: '{monitor_id_or_slug}', field: '_monitor', resource: 'monitor', where: { _organization: '{organization_id_or_slug}', 'project.id': '{project_id_or_slug}', id: '{monitor_id_or_slug}' } }, refresh: { list: 'listProjectMonitorCheckins', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
44
- _seq_token: { idPrefix: '', ids: '{n}' }, _seq_event: { idPrefix: '', ids: '{n}' }, _seq_envelope: { idPrefix: '', ids: '{n}' },
45
+ _seq_token: { idPrefix: '', ids: '{n}' }, _seq_event: { idPrefix: '', ids: '{n}' }, _seq_envelope: { idPrefix: '', ids: '{n}' }, _seq_project_key: { idPrefix: '', ids: '{n}' },
45
46
  },
46
47
  unmodeled: [
47
48
  'getOrganization', 'listProjectEvents', 'getProjectRelease', 'getProjectMonitor',
@@ -8,6 +8,7 @@ export const manifest: DerivedManifest = {
8
8
  // both lanes are one service, so one store: the ingest lane writes events the API lane reads, and the doors' accounts
9
9
  // are the ingest lane's too (aws's lanes likewise)
10
10
  service: 'sentry',
11
+ hosts: [{ host: 'sentry.io' }, { host: 'us.sentry.io' }, { host: 'de.sentry.io' }],
11
12
  body: { json: 'always' },
12
13
  ids: { template: '{uuid}' },
13
14
  time: 'iso',
@@ -25,10 +26,10 @@ export const manifest: DerivedManifest = {
25
26
  // source: https://docs.sentry.io/api/pagination/ "Pagination in the API is handled via the Link header standard:"
26
27
  resources: {
27
28
  // source: https://docs.sentry.io/api/projects/create-a-new-project/ "6758470122493650"
28
- organization: { idPrefix: '', alternateKeys: ['slug'], ids: '{digits:16}', refresh: { list: 'listOrganizations', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
29
+ organization: { idPrefix: '', unique: [{ fields: ['slug'], refusal: { status: 409, message: 'An organization with this slug already exists.' } }], alternateKeys: ['slug'], ids: '{digits:16}', refresh: { list: 'listOrganizations', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
29
30
  // source: spec:listOrganizationProjects "Return a list of projects bound to a organization"
30
31
  // source: https://docs.sentry.io/api/projects/create-a-new-project/ "6758470122493650"
31
- project: { idPrefix: '', alternateKeys: ['slug'], ids: '{digits:16}', parent: { param: 'organization_id_or_slug', field: '_organization', resource: 'organization' }, refresh: { list: 'listOrganizationProjects', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
32
+ project: { idPrefix: '', unique: [{ fields: ['_organization', 'slug'], refusal: { status: 409, message: 'A project with this slug already exists.' } }], alternateKeys: ['slug'], ids: '{digits:16}', parent: { param: 'organization_id_or_slug', field: '_organization', resource: 'organization' }, refresh: { list: 'listOrganizationProjects', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
32
33
  project_key: { idPrefix: '', ids: '{hex:32}', refresh: { none: 'Client keys contain credential secrets. Credential custody keeps their secret-bearing reads out of refresh.' } },
33
34
  // source: spec:listProjectEvents "Return a list of events bound to a project."
34
35
  event: { idPrefix: '', ids: '{hex:32}', key: '{project_id_or_slug}:{event_id}', vendorUnderscored: ['_meta'], ...states.event,
@@ -43,7 +44,7 @@ export const manifest: DerivedManifest = {
43
44
  // source: spec:listProjectMonitorCheckins "Retrieve Check-Ins for a Monitor by Project"
44
45
  // source: https://raw.githubusercontent.com/getsentry/sentry/2efdd4760a75a3d395e3534b7f66a7fbb0c3f22e/src/sentry/monitors/serializers.py "str(obj.guid)"
45
46
  check_in: { ...states.check_in, idPrefix: '', ids: '{uuid}', parent: { params: ['organization_id_or_slug', 'project_id_or_slug', 'monitor_id_or_slug'], value: '{monitor_id_or_slug}', field: '_monitor', resource: 'monitor', where: { _organization: '{organization_id_or_slug}', 'project.id': '{project_id_or_slug}', id: '{monitor_id_or_slug}' } }, refresh: { list: 'listProjectMonitorCheckins', items: '$body', nextLink: { header: 'link', rel: 'next', query: 'cursor', when: { results: 'true' } } } },
46
- _seq_token: { idPrefix: '', ids: '{n}' }, _seq_event: { idPrefix: '', ids: '{n}' }, _seq_envelope: { idPrefix: '', ids: '{n}' },
47
+ _seq_token: { idPrefix: '', ids: '{n}' }, _seq_event: { idPrefix: '', ids: '{n}' }, _seq_envelope: { idPrefix: '', ids: '{n}' }, _seq_project_key: { idPrefix: '', ids: '{n}' },
47
48
  },
48
49
  unmodeled: [
49
50
  'getOrganization', 'listProjectEvents', 'getProjectRelease', 'getProjectMonitor',
@@ -1,5 +1,5 @@
1
1
  import { gunzip } from '@volter/world-core';
2
- import { bearerToken, detail, EVENT, forbidden, grants, keyOf, makeProject, notFound, orgFor, PROJECT, projectFor, projectOf, projectView, wire } from "./shared.js";
2
+ import { bearerToken, detail, EVENT, forbidden, grants, keyOf, makeProject, notFound, orgFor, PROJECT, projectFor, projectOf, wire } from "./shared.js";
3
3
  // Releases, as sentry-cli makes, finalizes and associates commits with them (`releases new`, `releases finalize`,
4
4
  // `releases set-commits`), for one project at its path or for the organization; and the organization's repositories the
5
5
  // commits are read against. A release is the organization's, by its version, linked to the projects it was made for.
@@ -220,8 +220,10 @@ export async function createOrganizationProject(ctx) {
220
220
  if (projectOf(ctx, org, slug))
221
221
  return Response.json({ slug: ['A project with this slug already exists.'] }, { status: 409 });
222
222
  const project = await makeProject(ctx, org, slug, typeof b.platform === 'string' ? b.platform : null);
223
+ if (project instanceof Response)
224
+ return project;
223
225
  await ctx.write(PROJECT, String(project.id), { name: b.name }, 'project.update');
224
- return Response.json(projectView(ctx, ctx.row(PROJECT, String(project.id)), org), { status: 201 });
226
+ return Response.json(wire(ctx, ctx.row(PROJECT, String(project.id))), { status: 201 });
225
227
  }
226
228
  // source: spec:listProjectKeys "Return a list of client keys bound to a project."
227
229
  export async function listProjectKeys(ctx) {
@@ -1,5 +1,5 @@
1
1
  import { gunzip, type HandlerContext } from '@volter/world-core';
2
- import { bearerToken, detail, EVENT, forbidden, grants, keyOf, makeProject, notFound, orgFor, PROJECT, projectFor, projectOf, projectView, type Row, wire } from './shared.ts';
2
+ import { bearerToken, detail, EVENT, forbidden, grants, keyOf, makeProject, notFound, orgFor, PROJECT, projectFor, projectOf, type Row, wire } from './shared.ts';
3
3
 
4
4
  // Releases, as sentry-cli makes, finalizes and associates commits with them (`releases new`, `releases finalize`,
5
5
  // `releases set-commits`), for one project at its path or for the organization; and the organization's repositories the
@@ -199,8 +199,9 @@ export async function createOrganizationProject(ctx: HandlerContext): Promise<Re
199
199
  if (!/^(?![0-9]+$)[a-z0-9_-]+$/.test(slug)) return Response.json({ slug: ['Enter a valid slug.'] }, { status: 400 });
200
200
  if (projectOf(ctx, org, slug)) return Response.json({ slug: ['A project with this slug already exists.'] }, { status: 409 });
201
201
  const project = await makeProject(ctx, org, slug, typeof b.platform === 'string' ? b.platform : null);
202
+ if (project instanceof Response) return project;
202
203
  await ctx.write(PROJECT, String(project.id), { name: b.name }, 'project.update');
203
- return Response.json(projectView(ctx, ctx.row(PROJECT, String(project.id))!, org), { status: 201 });
204
+ return Response.json(wire(ctx, ctx.row(PROJECT, String(project.id))!), { status: 201 });
204
205
  }
205
206
 
206
207
  // source: spec:listProjectKeys "Return a list of client keys bound to a project."
@@ -1,8 +1,5 @@
1
1
  import type { HandlerContext } from '@volter/world-core';
2
- /** Bootstrap (RECIPE, State): the account's sign-up creates its first credential and env-named project/key.
3
- * The World's application: the organization `world`, its project `app`, the project's DSN and a token of the
4
- * organization's, made the first time and answered the same at every boot: the token is re-derived from its secret and
5
- * answered while it matches its hash; one revoked is not made again. */
2
+ /** The deployment's project client key and an independently issued auth token in its organization. */
6
3
  export declare function appCredentials(ctx: HandlerContext): Promise<Response>;
7
4
  export declare function organizations(ctx: HandlerContext): Promise<Response>;
8
5
  export declare function authTokens(ctx: HandlerContext): Promise<Response>;
@@ -1,27 +1,62 @@
1
- import { dsnOf, heldOrgToken, keyOf, makeOrg, makeOrgToken, makePersonalToken, makeProject, orgBySlug, TOKEN } from "./shared.js";
1
+ import { CLIENT_KEY, dsnOf, keyOf, makeClientKey, makeOrg, makeOrgToken, makePersonalToken, makeProject, orgBySlug, ORG, PROJECT, readToken, TOKEN } from "./shared.js";
2
2
  const bad = (error) => Response.json({ error }, { status: 400 });
3
3
  const bodyOf = (ctx) => (ctx.body && typeof ctx.body === 'object' ? ctx.body : {});
4
4
  const SLUG = /^[a-z0-9][a-z0-9_-]{0,49}$/;
5
- /** Bootstrap (RECIPE, State): the account's sign-up creates its first credential and env-named project/key.
6
- * The World's application: the organization `world`, its project `app`, the project's DSN and a token of the
7
- * organization's, made the first time and answered the same at every boot: the token is re-derived from its secret and
8
- * answered while it matches its hash; one revoked is not made again. */
5
+ /** The deployment's project client key and an independently issued auth token in its organization. */
9
6
  export async function appCredentials(ctx) {
10
- const b = bodyOf(ctx);
11
- const org = await makeOrg(ctx, String(b.organization ?? 'world'), String(b.name ?? 'World'));
12
- const project = await makeProject(ctx, org, String(b.project ?? 'app'), String(b.platform ?? 'javascript'));
13
- const key = keyOf(ctx, project);
14
- const revoked = ctx.rowsRaw(TOKEN, { withDeleted: true }).some((t) => t.deleted === true && t.organization === org.id && t.name === 'world-app');
15
- if (revoked)
16
- return Response.json({ error: 'the application token was revoked' }, { status: 409 });
17
- const token = (await heldOrgToken(ctx, org, 'world-app')) ?? await makePersonalToken(ctx, org, 'world-app', ['org:read', 'project:read', 'project:write', 'project:releases', 'event:read', 'alerts:read']);
18
- return Response.json({ dsn: dsnOf(org, project, key), auth_token: token, org: org.slug, org_id: org.id, project: project.slug }, { status: 201 });
7
+ try {
8
+ const b = bodyOf(ctx);
9
+ const organizationId = await ctx.account();
10
+ const org = await ctx.tenant(ORG, async (organization) => {
11
+ const label = typeof b.organization === 'string' ? b.organization : organization.id === 'default' ? 'world' : organization.id;
12
+ const rule = { max: 50, lower: true, allowed: /[a-z0-9_-]/, first: /[a-z0-9]/ };
13
+ const name = typeof b.name === 'string' ? b.name : organization.label;
14
+ let made = await makeOrg(ctx, ctx.vendorName(label, rule, { identity: organization.id }), name, organization.members[0]);
15
+ if (made instanceof Response)
16
+ made = await makeOrg(ctx, ctx.vendorName(label, rule, { suffixed: true, identity: organization.id }), name, organization.members[0]);
17
+ if (made instanceof Response)
18
+ throw await duplicate(made, organization.id);
19
+ return String(made.id);
20
+ });
21
+ const project = await ctx.deploymentObject(`project:${String(org.id)}`, PROJECT, async (deployment) => {
22
+ const label = typeof b.project === 'string' ? b.project : deployment || 'app';
23
+ const identity = deployment === '' ? organizationId : `${organizationId}/${deployment}`;
24
+ const rule = { max: Number.MAX_SAFE_INTEGER, lower: true, allowed: /[a-z0-9_-]/, first: /[a-z_-]/ };
25
+ const platform = typeof b.platform === 'string' ? b.platform : 'javascript';
26
+ let made = await makeProject(ctx, org, ctx.vendorName(label, rule, { identity }), platform);
27
+ if (made instanceof Response)
28
+ made = await makeProject(ctx, org, ctx.vendorName(label, rule, { suffixed: true, identity }), platform);
29
+ if (made instanceof Response)
30
+ throw await duplicate(made, identity);
31
+ return String(made.id);
32
+ });
33
+ const key = await ctx.accountKey('client-key', CLIENT_KEY, {
34
+ make: async () => String((keyOf(ctx, project) ?? await makeClientKey(ctx, org, project)).id),
35
+ read: async (row) => row._project === project.id && row.isActive !== false && typeof row.public === 'string' ? row.public : undefined,
36
+ });
37
+ const token = await ctx.accountKey('auth-token', TOKEN, {
38
+ make: async () => ctx.crypto.sha256(await makePersonalToken(ctx, org, ctx.deployment() || 'world-app', ['org:read', 'project:read', 'project:write', 'project:releases', 'event:read', 'alerts:read'])),
39
+ read: (row) => readToken(ctx, org, row),
40
+ });
41
+ return Response.json({ dsn: dsnOf(org, project, { public: key.secret }), auth_token: token.secret, authorization: `Bearer ${token.secret}`, url: 'https://sentry.io', org: org.slug, org_id: org.id, project: project.slug }, { status: 201 });
42
+ }
43
+ catch (error) {
44
+ if (error instanceof Response)
45
+ return error;
46
+ throw error;
47
+ }
48
+ }
49
+ async function duplicate(response, identity) {
50
+ const body = await response.json();
51
+ return Response.json({ slug: body.slug.map((message) => `${message} (${identity})`) }, { status: response.status });
19
52
  }
20
53
  export async function organizations(ctx) {
21
54
  const b = bodyOf(ctx);
22
55
  if (typeof b.slug !== 'string' || !SLUG.test(b.slug))
23
56
  return bad('a slug (lowercase letters, digits, - and _) is required');
24
- const org = await makeOrg(ctx, b.slug, typeof b.name === 'string' ? b.name : b.slug);
57
+ const org = await makeOrg(ctx, b.slug, typeof b.name === 'string' ? b.name : b.slug, (await ctx.organization()).members[0]);
58
+ if (org instanceof Response)
59
+ return org;
25
60
  return Response.json({ id: org.id, slug: org.slug }, { status: 201 });
26
61
  }
27
62
  export async function authTokens(ctx) {
@@ -32,8 +67,5 @@ export async function authTokens(ctx) {
32
67
  if (typeof b.name !== 'string' || !b.name)
33
68
  return bad('a name is required');
34
69
  const scopes = Array.isArray(b.scopes) ? b.scopes.map(String) : ['org:read', 'project:read', 'event:read'];
35
- return b.kind === 'personal' ? Response.json({ token: await makePersonalToken(ctx, org, String(b.name), scopes) }, { status: 201 }) : issueOrganizationToken(ctx, org, String(b.name));
36
- }
37
- async function issueOrganizationToken(ctx, org, name) {
38
- return Response.json({ token: await makeOrgToken(ctx, org, name) }, { status: 201 });
70
+ return b.kind === 'personal' ? Response.json({ token: await makePersonalToken(ctx, org, String(b.name), scopes) }, { status: 201 }) : Response.json({ token: await makeOrgToken(ctx, org, String(b.name)) }, { status: 201 });
39
71
  }
@@ -8,31 +8,61 @@
8
8
  // an organization auth token (Settings > Auth Tokens), or with kind
9
9
  // personal a personal token with its scopes, shown once
10
10
  import type { HandlerContext } from '@volter/world-core';
11
- import { dsnOf, heldOrgToken, keyOf, makeOrg, makeOrgToken, makePersonalToken, makeProject, orgBySlug, TOKEN } from './shared.ts';
11
+ import { CLIENT_KEY, dsnOf, keyOf, makeClientKey, makeOrg, makeOrgToken, makePersonalToken, makeProject, orgBySlug, ORG, PROJECT, readToken, TOKEN } from './shared.ts';
12
12
 
13
13
  const bad = (error: string): Response => Response.json({ error }, { status: 400 });
14
14
  const bodyOf = (ctx: HandlerContext): Record<string, unknown> => (ctx.body && typeof ctx.body === 'object' ? ctx.body : {}) as Record<string, unknown>;
15
15
  const SLUG = /^[a-z0-9][a-z0-9_-]{0,49}$/;
16
16
 
17
- /** Bootstrap (RECIPE, State): the account's sign-up creates its first credential and env-named project/key.
18
- * The World's application: the organization `world`, its project `app`, the project's DSN and a token of the
19
- * organization's, made the first time and answered the same at every boot: the token is re-derived from its secret and
20
- * answered while it matches its hash; one revoked is not made again. */
17
+ /** The deployment's project client key and an independently issued auth token in its organization. */
21
18
  export async function appCredentials(ctx: HandlerContext): Promise<Response> {
22
- const b = bodyOf(ctx);
23
- const org = await makeOrg(ctx, String(b.organization ?? 'world'), String(b.name ?? 'World'));
24
- const project = await makeProject(ctx, org, String(b.project ?? 'app'), String(b.platform ?? 'javascript'));
25
- const key = keyOf(ctx, project)!;
26
- const revoked = ctx.rowsRaw(TOKEN, { withDeleted: true }).some((t) => t.deleted === true && t.organization === org.id && t.name === 'world-app');
27
- if (revoked) return Response.json({ error: 'the application token was revoked' }, { status: 409 });
28
- const token = (await heldOrgToken(ctx, org, 'world-app')) ?? await makePersonalToken(ctx, org, 'world-app', ['org:read', 'project:read', 'project:write', 'project:releases', 'event:read', 'alerts:read']);
29
- return Response.json({ dsn: dsnOf(org, project, key), auth_token: token, org: org.slug, org_id: org.id, project: project.slug }, { status: 201 });
19
+ try {
20
+ const b = bodyOf(ctx);
21
+ const organizationId = await ctx.account();
22
+ const org = await ctx.tenant(ORG, async (organization) => {
23
+ const label = typeof b.organization === 'string' ? b.organization : organization.id === 'default' ? 'world' : organization.id;
24
+ const rule = { max: 50, lower: true, allowed: /[a-z0-9_-]/, first: /[a-z0-9]/ };
25
+ const name = typeof b.name === 'string' ? b.name : organization.label;
26
+ let made = await makeOrg(ctx, ctx.vendorName(label, rule, { identity: organization.id }), name, organization.members[0]!);
27
+ if (made instanceof Response) made = await makeOrg(ctx, ctx.vendorName(label, rule, { suffixed: true, identity: organization.id }), name, organization.members[0]!);
28
+ if (made instanceof Response) throw await duplicate(made, organization.id);
29
+ return String(made.id);
30
+ });
31
+ const project = await ctx.deploymentObject(`project:${String(org.id)}`, PROJECT, async (deployment) => {
32
+ const label = typeof b.project === 'string' ? b.project : deployment || 'app';
33
+ const identity = deployment === '' ? organizationId : `${organizationId}/${deployment}`;
34
+ const rule = { max: Number.MAX_SAFE_INTEGER, lower: true, allowed: /[a-z0-9_-]/, first: /[a-z_-]/ };
35
+ const platform = typeof b.platform === 'string' ? b.platform : 'javascript';
36
+ let made = await makeProject(ctx, org, ctx.vendorName(label, rule, { identity }), platform);
37
+ if (made instanceof Response) made = await makeProject(ctx, org, ctx.vendorName(label, rule, { suffixed: true, identity }), platform);
38
+ if (made instanceof Response) throw await duplicate(made, identity);
39
+ return String(made.id);
40
+ });
41
+ const key = await ctx.accountKey('client-key', CLIENT_KEY, {
42
+ make: async () => String((keyOf(ctx, project) ?? await makeClientKey(ctx, org, project)).id),
43
+ read: async (row) => row._project === project.id && row.isActive !== false && typeof row.public === 'string' ? row.public : undefined,
44
+ });
45
+ const token = await ctx.accountKey('auth-token', TOKEN, {
46
+ make: async () => ctx.crypto.sha256(await makePersonalToken(ctx, org, ctx.deployment() || 'world-app', ['org:read', 'project:read', 'project:write', 'project:releases', 'event:read', 'alerts:read'])),
47
+ read: (row) => readToken(ctx, org, row),
48
+ });
49
+ return Response.json({ dsn: dsnOf(org, project, { public: key.secret }), auth_token: token.secret, authorization: `Bearer ${token.secret}`, url: 'https://sentry.io', org: org.slug, org_id: org.id, project: project.slug }, { status: 201 });
50
+ } catch (error) {
51
+ if (error instanceof Response) return error;
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ async function duplicate(response: Response, identity: string): Promise<Response> {
57
+ const body = await response.json() as { slug: string[] };
58
+ return Response.json({ slug: body.slug.map((message) => `${message} (${identity})`) }, { status: response.status });
30
59
  }
31
60
 
32
61
  export async function organizations(ctx: HandlerContext): Promise<Response> {
33
62
  const b = bodyOf(ctx);
34
63
  if (typeof b.slug !== 'string' || !SLUG.test(b.slug)) return bad('a slug (lowercase letters, digits, - and _) is required');
35
- const org = await makeOrg(ctx, b.slug, typeof b.name === 'string' ? b.name : b.slug);
64
+ const org = await makeOrg(ctx, b.slug, typeof b.name === 'string' ? b.name : b.slug, (await ctx.organization()).members[0]!);
65
+ if (org instanceof Response) return org;
36
66
  return Response.json({ id: org.id, slug: org.slug }, { status: 201 });
37
67
  }
38
68
 
@@ -42,9 +72,5 @@ export async function authTokens(ctx: HandlerContext): Promise<Response> {
42
72
  if (!org) return bad('organization must name an organization the World holds');
43
73
  if (typeof b.name !== 'string' || !b.name) return bad('a name is required');
44
74
  const scopes = Array.isArray(b.scopes) ? b.scopes.map(String) : ['org:read', 'project:read', 'event:read'];
45
- return b.kind === 'personal' ? Response.json({ token: await makePersonalToken(ctx, org, String(b.name), scopes) }, { status: 201 }) : issueOrganizationToken(ctx, org, String(b.name));
46
- }
47
-
48
- async function issueOrganizationToken(ctx: HandlerContext, org: Record<string, unknown>, name: string): Promise<Response> {
49
- return Response.json({ token: await makeOrgToken(ctx, org, name) }, { status: 201 });
75
+ return b.kind === 'personal' ? Response.json({ token: await makePersonalToken(ctx, org, String(b.name), scopes) }, { status: 201 }) : Response.json({ token: await makeOrgToken(ctx, org, String(b.name)) }, { status: 201 });
50
76
  }
@@ -12,4 +12,3 @@ export declare function projectFor(ctx: HandlerContext, scope: string): {
12
12
  org: Row;
13
13
  project: Row;
14
14
  } | Response;
15
- export declare function projectView(ctx: HandlerContext, p: Row, _org: Row): Row;