@theholocron/holocron-plugin-sentry 5.0.0-alpha.44 → 5.0.0-alpha.442

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,7 @@ Token resolution order:
21
21
  2. `HOLOCRON_SENTRY_TOKEN` env var
22
22
  3. `SENTRY_AUTH_TOKEN` env var (the standard Sentry variable name)
23
23
  4. Keyring `sentry.<org>` — tried first when an org is active via `--org`, `HOLOCRON_ORG`, or `org` in `holocron.config.ts`
24
- 5. Keyring `sentry` — unnamespaced fallback; set via `holocron auth set sentry <token>`
24
+ 5. Keyring `sentry` — unnamespaced fallback; set via `holocron new token sentry <token>`
25
25
 
26
26
  Generate an auth token at **sentry.io/settings/account/api/auth-tokens/**
27
27
  with `org:read`, `project:read`, and `project:write` scopes. Both
@@ -47,12 +47,15 @@ user-owned and org-owned tokens are accepted.
47
47
 
48
48
  | Method | What it does |
49
49
  | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
50
- | `describe` | Returns `{ provider: "sentry", envKeys: ["SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN"] }` — the env vars the app reads at runtime. |
50
+ | `describe` | Returns `{ provider: "sentry", envKeys: ["SENTRY_DSN"], frameworkEnvKeys: { next: ["NEXT_PUBLIC_SENTRY_DSN"] } }` — the env vars the app reads at runtime. |
51
51
  | `whoami` | Fetches the org by slug to verify the token and confirm the org exists. |
52
52
  | `ensureProject` | Looks up the project by slug (derived from `name`); creates it under the configured team if absent. Returns the DSN and an `alreadyExists` flag. Idempotent. |
53
53
 
54
54
  A provider entry in `holocron.config` is only needed to enable `holocron
55
- setup` provisioning (`ensureProject` + pushing `SENTRY_DSN` /
56
- `NEXT_PUBLIC_SENTRY_DSN` to Secrets) and `holocron doctor` connectivity
55
+ setup` provisioning (`ensureProject` + pushing `SENTRY_DSN` to Secrets, and
56
+ `NEXT_PUBLIC_SENTRY_DSN` too when Next.js is detected: the root `package.json`
57
+ or any workspace package's lists `next`) and `holocron doctor` connectivity
57
58
  checks — not for runtime error reporting, which the CLI activates directly
58
- from `HOLOCRON_SENTRY_DSN` (fallback `SENTRY_DSN`).
59
+ from `HOLOCRON_SENTRY_DSN` (fallback `SENTRY_DSN`). A DSN is built to be embedded in
60
+ client code, so it is not a true secret.
61
+ Setup never removes `NEXT_PUBLIC_SENTRY_DSN`: a repo that drops Next.js deletes that secret by hand.
package/dist/index.d.mts CHANGED
@@ -1,28 +1,55 @@
1
- import { AuthError, Errors, ResolveTokenInput } from "@theholocron/cli";
1
+ import { AuthError, EnsureRelayInput, EnsureRelayResult, Errors, InspectRelayInput, PluginToken, RelayState, ResolveTokenInput, VerifyTokenFailure, VerifyTokenResult, VerifyTokenResult as VerifyTokenResult$1, VerifyTokenSuccess } from "@theholocron/cli";
2
2
  import { SentryClient, SentryClientOptions, createSentryClient } from "@theholocron/sentry-client";
3
- //#region src/auth.d.ts
4
- declare const resolveToken: (input?: ResolveTokenInput) => string;
5
- //#endregion
6
- //#region src/capabilities/errors.d.ts
3
+ /** Resolves the Sentry token from `--token`, `HOLOCRON_SENTRY_TOKEN`, `SENTRY_AUTH_TOKEN` or the keyring, and throws an `AuthError` when none is set. */
4
+ export declare const resolveToken: import("@theholocron/cli").TokenResolver;
5
+ /**
6
+ * The token for the Comlink relay: an integration, its client secret and an alert rule need more than the general token
7
+ * (`alerts:write`, `org:write`, `event:read`), so it has its own.
8
+ */
9
+ export declare const resolveComlinkToken: import("@theholocron/cli").FeatureResolver;
10
+ /**
11
+ * The token for the relay: the Comlink one when there is one, else the general Sentry token as the last resort. The
12
+ * fallback only helps if the general token happens to carry the scopes; otherwise Sentry's answer names what is missing.
13
+ */
14
+ export declare function resolveRelayToken(input?: ResolveTokenInput): string;
15
+ /**
16
+ * Every token this plugin takes, for `holocron doctor secrets --verify`, each with what its layer's token must and must not be
17
+ * able to do: the general one, the Comlink relay's own and the CI release token.
18
+ */
19
+ export declare const tokens: PluginToken[];
20
+ /** Options for the Sentry `errors` capability. */
7
21
  interface SentryErrorsOptions {
8
22
  /** Sentry organization slug. Required for `whoami` and `ensureProject`. */
9
23
  org?: string;
10
24
  /** Default team slug for project creation. Defaults to org slug. */
11
25
  team?: string;
12
26
  }
13
- declare class SentryErrors implements Errors {
27
+ /** The `errors` capability for Sentry: finds or creates a project and reports its DSN, and manages the Comlink relay. */
28
+ export declare class SentryErrors implements Errors {
14
29
  private readonly client;
15
30
  private readonly opts;
31
+ /** The client for the relay, made with the Comlink token (see `resolveRelayToken`). */
32
+ private readonly relayClient;
33
+ /** The capability key, always `errors`. */
16
34
  readonly key: "errors";
35
+ /** The provider name, `sentry`. */
17
36
  readonly providerName = "sentry";
18
- constructor(client: () => SentryClient, opts: SentryErrorsOptions);
37
+ constructor(client: () => SentryClient, opts: SentryErrorsOptions,
38
+ /** The client for the relay, made with the Comlink token (see `resolveRelayToken`). */
39
+ relayClient?: () => SentryClient);
40
+ ensureRelay(input: EnsureRelayInput): Promise<EnsureRelayResult>;
41
+ inspectRelay(input: InspectRelayInput): Promise<RelayState>;
19
42
  describe(): Promise<{
20
43
  provider: string;
21
44
  envKeys: string[];
45
+ frameworkEnvKeys: {
46
+ next: string[];
47
+ };
22
48
  }>;
23
49
  whoami(): Promise<{
24
50
  org: string;
25
- }>;
51
+ } | null>;
52
+ /** Returns the public DSN of the project named `input.name` (matched by its slug), creating it when missing; needs `org`, and the new project uses `team` (default the org slug) and `platform` (default `node`). */
26
53
  ensureProject(input: {
27
54
  name: string;
28
55
  platform?: string;
@@ -37,25 +64,14 @@ declare class SentryErrors implements Errors {
37
64
  */
38
65
  private requireOrg;
39
66
  }
40
- //#endregion
41
- //#region src/verify-token.d.ts
42
- interface VerifyTokenSuccess {
43
- ok: true;
44
- subject: string;
45
- }
46
- interface VerifyTokenFailure {
47
- ok: false;
48
- message: string;
49
- }
50
- type VerifyTokenResult = VerifyTokenSuccess | VerifyTokenFailure;
51
67
  interface VerifyTokenOptions {
52
68
  baseUrl?: string;
53
69
  fetch?: typeof fetch;
54
70
  }
55
- declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult>;
56
- //#endregion
57
- //#region src/index.d.ts
58
- interface SentryPluginOptions extends ResolveTokenInput, SentryErrorsOptions {
71
+ /** Checks a Sentry token by listing its organizations; the success subject is `org:` and the first organization's slug, the failure carries the error message. */
72
+ export declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult$1>;
73
+ /** Options for the Sentry plugin: token resolution, the organization and team, and test overrides. */
74
+ export interface SentryPluginOptions extends ResolveTokenInput, SentryErrorsOptions {
59
75
  /**
60
76
  * Sentry organization slug. Optional at load time — the `errors` capability
61
77
  * activates from env vars alone — but required for `whoami` / `ensureProject`
@@ -64,20 +80,33 @@ interface SentryPluginOptions extends ResolveTokenInput, SentryErrorsOptions {
64
80
  org?: string;
65
81
  /** Override base URL for tests. */
66
82
  baseUrl?: string;
83
+ /** Override `fetch` for tests. */
67
84
  fetch?: typeof fetch;
68
85
  }
69
- interface PluginContext {
86
+ /** What the plugin's capabilities share: the options and two lazily built clients. */
87
+ export interface PluginContext {
88
+ /** The options the plugin was created with. */
70
89
  options: SentryPluginOptions;
90
+ /** Builds the Sentry client on first call and reuses it; the token is resolved then, not at plugin load. */
71
91
  client: () => SentryClient;
92
+ /** The client for the Comlink relay: its own token (`sentry.comlink`), the general one only as a last resort. */
93
+ relayClient: () => SentryClient;
72
94
  }
73
- declare function createContext(options: SentryPluginOptions): PluginContext;
74
- declare function errors(ctx: PluginContext): Errors;
75
- declare function createPlugin(options: SentryPluginOptions): {
95
+ /** Creates the shared context for the given options; no token is resolved until a client is first called. */
96
+ export declare function createContext(options: SentryPluginOptions): PluginContext;
97
+ /** Builds the `errors` capability from a plugin context, using the relay client for the Comlink relay. */
98
+ export declare function errors(ctx: PluginContext): Errors;
99
+ /** Creates the Sentry plugin: its probes, a `connection` probe and the `errors` capability. */
100
+ export declare function createPlugin(options: SentryPluginOptions): {
76
101
  name: string;
102
+ probes: {
103
+ token: import("@theholocron/cli").Probe;
104
+ connection: import("@theholocron/cli").Probe;
105
+ };
77
106
  capabilities: {
78
107
  errors: () => Errors;
79
108
  };
80
109
  };
81
- declare const AUTH_HINT: string;
82
- //#endregion
83
- export { AUTH_HINT, AuthError, PluginContext, type ResolveTokenInput, type SentryClient, type SentryClientOptions, SentryErrors, type SentryErrorsOptions, SentryPluginOptions, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createContext, createPlugin, createSentryClient, errors, resolveToken, verifyToken };
110
+ /** One-line hint shown by `holocron new token sentry` when no token is given or the token is rejected. */
111
+ export declare const AUTH_HINT: string;
112
+ export { AuthError, type ResolveTokenInput, type SentryClient, type SentryClientOptions, type SentryErrorsOptions, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createSentryClient };
package/dist/index.mjs CHANGED
@@ -1,32 +1,366 @@
1
- import { AuthError, ProviderApiError, createResolveToken } from "@theholocron/cli";
1
+ import { AuthError, ProviderApiError, connectionProbe, createFeatureResolver, createResolveToken, tokenExpectations } from "@theholocron/cli";
2
+ import { createEnvLookup, holocronEnv } from "@theholocron/env-utils/lookup";
3
+ import { randomUUID } from "node:crypto";
4
+ import { errorMessage } from "@theholocron/misc-utils";
2
5
  import { createSentryClient } from "@theholocron/sentry-client";
3
- //#region src/auth.ts
6
+ // src/probes/token/expectations.ts
7
+ const READS = {
8
+ org: "required",
9
+ projects: "required"
10
+ };
11
+ /**
12
+ * The general token provisions projects and their DSNs and lives on a laptop (`local` inherits `dev`): it needs
13
+ * `org:read` and `project:read`. `project:write` has no read that proves it, so it is not probed; the permission table
14
+ * in `docs/src/content/docs/tokens.mdx` lists it. This is the plugin's `tokens` declaration, read through
15
+ * `tokenExpectations()`; change it and that table together.
16
+ */
17
+ const GENERAL = { permissions: {
18
+ local: READS,
19
+ dev: READS
20
+ } };
21
+ const RELAY_READS = {
22
+ org: "required",
23
+ projects: "required",
24
+ alerts: "required"
25
+ };
26
+ /**
27
+ * The Comlink relay's token makes an integration and an alert rule, so on top of the general token's reads it needs
28
+ * `alerts:read`. Its write scopes (`org:write`, `alerts:write`) and `event:read` are not probed, as no read proves them.
29
+ */
30
+ const COMLINK = { permissions: {
31
+ local: RELAY_READS,
32
+ dev: RELAY_READS
33
+ } };
34
+ /**
35
+ * The release token only creates a release and uploads sourcemaps (an organization token with `org:ci`), so it must not
36
+ * read the organization, its projects or its alert rules: a leak from CI cannot reach them.
37
+ */
38
+ const RELEASE = { permissions: { ci: {
39
+ org: "absent",
40
+ projects: "absent",
41
+ alerts: "absent"
42
+ } } };
43
+ // src/auth.ts
44
+ /** Resolves the Sentry token from `--token`, `HOLOCRON_SENTRY_TOKEN`, `SENTRY_AUTH_TOKEN` or the keyring, and throws an `AuthError` when none is set. */
4
45
  const resolveToken = createResolveToken({
5
46
  envName: "HOLOCRON_SENTRY_TOKEN",
6
47
  vendorEnvName: "SENTRY_AUTH_TOKEN",
7
48
  keyringService: "sentry",
8
- errorMessage: "no Sentry auth token found. Pass --token <TOKEN>, set HOLOCRON_SENTRY_TOKEN / SENTRY_AUTH_TOKEN, or run: holocron auth set sentry <TOKEN>"
49
+ errorMessage: "no Sentry auth token found. Pass --token <TOKEN>, set HOLOCRON_SENTRY_TOKEN / SENTRY_AUTH_TOKEN, or run: holocron new token sentry <TOKEN>"
9
50
  });
10
- //#endregion
11
- //#region src/capabilities/errors.ts
51
+ /**
52
+ * The token for the Comlink relay: an integration, its client secret and an alert rule need more than the general token
53
+ * (`alerts:write`, `org:write`, `event:read`), so it has its own.
54
+ */
55
+ const resolveComlinkToken = createFeatureResolver({
56
+ envName: "HOLOCRON_SENTRY_COMLINK_TOKEN",
57
+ keyringKey: "sentry.comlink",
58
+ vendor: "Sentry"
59
+ });
60
+ /**
61
+ * The token for the relay: the Comlink one when there is one, else the general Sentry token as the last resort. The
62
+ * fallback only helps if the general token happens to carry the scopes; otherwise Sentry's answer names what is missing.
63
+ */
64
+ function resolveRelayToken(input = {}) {
65
+ try {
66
+ return resolveComlinkToken(input);
67
+ } catch (err) {
68
+ if (!(err instanceof AuthError)) throw err;
69
+ return resolveToken(input);
70
+ }
71
+ }
72
+ /**
73
+ * The token only `release.yml` reads (sentry-cli makes the release and uploads sourcemaps): an organization token with
74
+ * `org:ci` held in the `ci` layer. The plugin never resolves it, so it is `optional` and `verifiable: false` (the
75
+ * plugin's `verifyToken` lists organizations, which this token must not be able to do); it is declared so the probe and
76
+ * `holocron probe sentry token --environment ci` can find it and check that it reaches nothing else.
77
+ */
78
+ const releaseToken = {
79
+ envName: "HOLOCRON_SENTRY_RELEASE_TOKEN",
80
+ keyringKey: "sentry.release",
81
+ optional: true,
82
+ verifiable: false,
83
+ expect: RELEASE
84
+ };
85
+ /**
86
+ * Every token this plugin takes, for `holocron doctor secrets --verify`, each with what its layer's token must and must not be
87
+ * able to do: the general one, the Comlink relay's own and the CI release token.
88
+ */
89
+ const tokens = [
90
+ {
91
+ ...resolveToken.token,
92
+ expect: GENERAL
93
+ },
94
+ {
95
+ ...resolveComlinkToken.token,
96
+ expect: COMLINK
97
+ },
98
+ releaseToken
99
+ ];
100
+ // src/capabilities/relay.ts
101
+ /** What the integration needs to read: the alert's event. It is also why reading its secret needs the same scope. */
102
+ const INTEGRATION_SCOPES = ["event:read"];
103
+ const DEFAULT_FREQUENCY = 5;
104
+ /**
105
+ * The alert action that sends through an integration, in the shape Sentry's own page writes when "Send a notification via
106
+ * an integration" is picked: a `webhook` action named by the integration's slug. (A `sentry_app` action with the numeric
107
+ * id is accepted too, but the page shows it as an unknown integration.)
108
+ */
109
+ const integrationAction = (slug) => ({
110
+ type: "webhook",
111
+ config: {
112
+ targetType: null,
113
+ targetDisplay: null,
114
+ targetIdentifier: slug
115
+ },
116
+ data: {}
117
+ });
118
+ const sendsThrough = (action, slug) => action.type === "webhook" && action.config["targetIdentifier"] === slug;
119
+ /** The `sentry_app` form an earlier version wrote for this integration, which the page cannot name. */
120
+ const isLegacyAction = (action, appId) => action.type === "sentry_app" && String(action.config["targetIdentifier"]) === String(appId);
121
+ /** A workflow as Sentry returned it, as the body to send back (the nested ids are not part of an update). */
122
+ function toInput(wf) {
123
+ return {
124
+ name: wf.name,
125
+ enabled: wf.enabled,
126
+ environment: wf.environment,
127
+ config: wf.config,
128
+ detectorIds: wf.detectorIds,
129
+ triggers: {
130
+ logicType: wf.triggers.logicType,
131
+ conditions: wf.triggers.conditions.map((c) => ({
132
+ type: c.type,
133
+ comparison: c.comparison,
134
+ conditionResult: c.conditionResult
135
+ }))
136
+ },
137
+ actionFilters: wf.actionFilters.map((f) => ({
138
+ logicType: f.logicType,
139
+ conditions: f.conditions.map((c) => ({
140
+ type: c.type,
141
+ comparison: c.comparison,
142
+ conditionResult: c.conditionResult
143
+ })),
144
+ actions: f.actions.map((a) => ({
145
+ type: a.type,
146
+ config: a.config,
147
+ data: a.data
148
+ }))
149
+ }))
150
+ };
151
+ }
152
+ /** Sentry's own "high priority issues" rule, which is the default for a new alert. */
153
+ function newWorkflow(name, frequency, detectorIds, slug) {
154
+ return {
155
+ name,
156
+ enabled: true,
157
+ environment: null,
158
+ config: { frequency },
159
+ detectorIds,
160
+ triggers: {
161
+ logicType: "any-short",
162
+ conditions: [{
163
+ type: "new_high_priority_issue",
164
+ comparison: true,
165
+ conditionResult: true
166
+ }, {
167
+ type: "existing_high_priority_issue",
168
+ comparison: true,
169
+ conditionResult: true
170
+ }]
171
+ },
172
+ actionFilters: [{
173
+ logicType: "all",
174
+ conditions: [],
175
+ actions: [integrationAction(slug)]
176
+ }]
177
+ };
178
+ }
179
+ /** The integration: found by name, made if missing, and its webhook and alert action brought in line. */
180
+ async function ensureIntegration(client, org, input) {
181
+ const existing = (await client.sentryApps.list(org)).find((a) => a.name === input.name);
182
+ if (!existing) return {
183
+ app: await client.sentryApps.create(org, {
184
+ name: input.name,
185
+ scopes: INTEGRATION_SCOPES,
186
+ webhookUrl: input.webhookUrl,
187
+ isAlertable: true
188
+ }),
189
+ created: true,
190
+ updated: []
191
+ };
192
+ const updated = [];
193
+ if (existing.webhookUrl !== input.webhookUrl) updated.push("webhook URL");
194
+ if (!existing.isAlertable) updated.push("alert action");
195
+ const scopes = [.../* @__PURE__ */ new Set([...existing.scopes, ...INTEGRATION_SCOPES])];
196
+ if (scopes.length !== existing.scopes.length) updated.push("scopes");
197
+ if (updated.length === 0) return {
198
+ app: existing,
199
+ created: false,
200
+ updated
201
+ };
202
+ return {
203
+ app: await client.sentryApps.update(existing.slug, {
204
+ name: existing.name,
205
+ scopes,
206
+ webhookUrl: input.webhookUrl,
207
+ isAlertable: true
208
+ }),
209
+ created: false,
210
+ updated
211
+ };
212
+ }
213
+ /** The issue stream detector of each project, so a wrong project fails before anything is changed. */
214
+ async function detectorsFor(client, org, slugs) {
215
+ const [projects, detectors] = await Promise.all([client.projects.list(org), client.workflows.detectors(org)]);
216
+ return slugs.map((slug) => {
217
+ const project = projects.find((p) => p.slug === slug);
218
+ if (!project) throw new Error(`Sentry project \`${slug}\` was not found in ${org}`);
219
+ const detector = detectors.find((d) => d.projectId === project.id && d.type === "issue_stream");
220
+ if (!detector) throw new Error(`Sentry project \`${slug}\` has no issue stream detector to attach an alert to`);
221
+ return detector.id;
222
+ });
223
+ }
224
+ /** The alert rule: one workflow for every project, found by name, made if missing, reconciled if not. */
225
+ async function ensureRule(client, org, input, install, detectorIds) {
226
+ const workflows = await client.workflows.list(org);
227
+ const frequency = input.rule.frequency ?? DEFAULT_FREQUENCY;
228
+ const existing = workflows.find((w) => w.name === input.rule.name);
229
+ if (!existing) {
230
+ await client.workflows.create(org, newWorkflow(input.rule.name, frequency, detectorIds, install.slug));
231
+ return {
232
+ created: true,
233
+ updated: []
234
+ };
235
+ }
236
+ const next = toInput(existing);
237
+ const updated = [];
238
+ if (!existing.enabled) {
239
+ next.enabled = true;
240
+ updated.push("enabled");
241
+ }
242
+ if (input.rule.frequency !== void 0 && existing.config.frequency !== input.rule.frequency) {
243
+ next.config = {
244
+ ...existing.config,
245
+ frequency: input.rule.frequency
246
+ };
247
+ updated.push("frequency");
248
+ }
249
+ const missing = detectorIds.filter((id) => !existing.detectorIds.includes(id));
250
+ if (missing.length > 0) {
251
+ next.detectorIds = [...existing.detectorIds, ...missing];
252
+ updated.push("projects");
253
+ }
254
+ const sends = existing.actionFilters.some((f) => f.actions.some((a) => sendsThrough(a, install.slug)));
255
+ const legacy = existing.actionFilters.some((f) => f.actions.some((a) => isLegacyAction(a, install.appId)));
256
+ if (legacy) for (const f of next.actionFilters) f.actions = f.actions.filter((a) => !isLegacyAction(a, install.appId));
257
+ if (!sends) {
258
+ const open = next.actionFilters.find((f) => f.conditions.length === 0);
259
+ if (open) open.actions.push(integrationAction(install.slug));
260
+ else next.actionFilters.push({
261
+ logicType: "all",
262
+ conditions: [],
263
+ actions: [integrationAction(install.slug)]
264
+ });
265
+ }
266
+ if (legacy || !sends) updated.push("action");
267
+ if (updated.length === 0) return {
268
+ created: false,
269
+ updated
270
+ };
271
+ await client.workflows.update(org, existing.id, next);
272
+ return {
273
+ created: false,
274
+ updated
275
+ };
276
+ }
277
+ /** The signing secret, if the token may see it: a new integration's answer omits it, so it is read again. */
278
+ async function secretOf(client, app) {
279
+ if (app.clientSecret) return app.clientSecret;
280
+ return (await client.sentryApps.get(app.slug)).clientSecret || void 0;
281
+ }
282
+ async function ensureSentryRelay(client, org, input) {
283
+ if (input.projects.length === 0) throw new Error("relay needs at least one Sentry project to alert on");
284
+ const detectorIds = await detectorsFor(client, org, input.projects);
285
+ const integration = await ensureIntegration(client, org, input);
286
+ const install = (await client.sentryApps.installations(org)).find((i) => i.app.slug === integration.app.slug);
287
+ if (!install) throw new Error(`the integration \`${integration.app.name}\` is not installed on ${org}`);
288
+ const rule = await ensureRule(client, org, input, {
289
+ slug: install.app.slug,
290
+ appId: install.app.sentryAppId
291
+ }, detectorIds);
292
+ const clientSecret = await secretOf(client, integration.app);
293
+ return {
294
+ integration: {
295
+ created: integration.created,
296
+ updated: integration.updated
297
+ },
298
+ rule,
299
+ ...clientSecret ? { clientSecret } : {}
300
+ };
301
+ }
302
+ /**
303
+ * Read the integration and the alert rule back, changing nothing. The integration's signing secret is not read: the
304
+ * list answer is used as it is and `get`, which returns the secret, is never called.
305
+ */
306
+ async function inspectSentryRelay(client, org, input) {
307
+ const apps = await client.sentryApps.list(org);
308
+ const projects = await client.projects.list(org);
309
+ const detectors = await client.workflows.detectors(org);
310
+ const workflows = await client.workflows.list(org);
311
+ const app = apps.find((a) => a.name === input.name);
312
+ const rule = workflows.find((w) => w.name === input.rule.name);
313
+ return {
314
+ ...app ? { integration: {
315
+ webhookUrl: app.webhookUrl,
316
+ alertAction: app.isAlertable
317
+ } } : {},
318
+ rules: rule ? input.projects.flatMap((slug) => {
319
+ const project = projects.find((p) => p.slug === slug);
320
+ const detector = project && detectors.find((d) => d.projectId === project.id && d.type === "issue_stream");
321
+ if (!detector || !rule.detectorIds.includes(detector.id)) return [];
322
+ const sends = app !== void 0 && rule.actionFilters.some((f) => f.actions.some((a) => sendsThrough(a, app.slug)));
323
+ return [{
324
+ project: slug,
325
+ enabled: rule.enabled,
326
+ sends
327
+ }];
328
+ }) : []
329
+ };
330
+ }
331
+ // src/capabilities/errors.ts
332
+ /** The `errors` capability for Sentry: finds or creates a project and reports its DSN, and manages the Comlink relay. */
12
333
  var SentryErrors = class {
13
334
  client;
14
335
  opts;
336
+ relayClient;
337
+ /** The capability key, always `errors`. */
15
338
  key = "errors";
339
+ /** The provider name, `sentry`. */
16
340
  providerName = "sentry";
17
- constructor(client, opts) {
341
+ constructor(client, opts, relayClient = client) {
18
342
  this.client = client;
19
343
  this.opts = opts;
344
+ this.relayClient = relayClient;
345
+ }
346
+ async ensureRelay(input) {
347
+ return ensureSentryRelay(this.relayClient(), this.requireOrg(), input);
348
+ }
349
+ async inspectRelay(input) {
350
+ return inspectSentryRelay(this.relayClient(), this.requireOrg(), input);
20
351
  }
21
352
  async describe() {
22
353
  return {
23
354
  provider: "sentry",
24
- envKeys: ["SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN"]
355
+ envKeys: ["SENTRY_DSN"],
356
+ frameworkEnvKeys: { next: ["NEXT_PUBLIC_SENTRY_DSN"] }
25
357
  };
26
358
  }
27
359
  async whoami() {
28
- return { org: (await this.client().auth.getOrg(this.requireOrg())).slug };
360
+ if (!this.opts.org) return null;
361
+ return { org: (await this.client().auth.getOrg(this.opts.org)).slug };
29
362
  }
363
+ /** Returns the public DSN of the project named `input.name` (matched by its slug), creating it when missing; needs `org`, and the new project uses `team` (default the org slug) and `platform` (default `node`). */
30
364
  async ensureProject(input) {
31
365
  const org = this.requireOrg();
32
366
  const slug = toSlug(input.name);
@@ -68,8 +402,401 @@ var SentryErrors = class {
68
402
  function toSlug(name) {
69
403
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
70
404
  }
71
- //#endregion
72
- //#region src/verify-token.ts
405
+ // src/probes/probe-deps.type.ts
406
+ function requireOrg(deps) {
407
+ if (!deps.org) throw new Error("the Sentry probes need `org` in the sentry provider's options");
408
+ return deps.org;
409
+ }
410
+ // src/probes/integrations.ts
411
+ /** Where a webhook points, without a query string that could carry a token. */
412
+ function where(url) {
413
+ if (!url) return "no webhook";
414
+ try {
415
+ const u = new URL(url);
416
+ return `${u.host}${u.pathname}`;
417
+ } catch {
418
+ return "an unreadable URL";
419
+ }
420
+ }
421
+ function integrationsProbe(deps) {
422
+ return {
423
+ summary: "List the custom integrations: scopes, alert action, webhook, and whether the client secret is visible.",
424
+ writes: false,
425
+ async run(probe) {
426
+ const apps = await deps.client().sentryApps.list(requireOrg(deps));
427
+ if (apps.length === 0) probe.say({
428
+ status: "info",
429
+ label: "no custom integrations"
430
+ });
431
+ for (const app of apps) {
432
+ probe.secret(app.clientSecret);
433
+ probe.say({
434
+ status: "ok",
435
+ label: `${app.name} (${app.slug})`,
436
+ detail: [
437
+ `scopes: ${app.scopes.join(", ") || "none"}`,
438
+ `alert action: ${app.isAlertable ? "on" : "off"}`,
439
+ `webhook: ${where(app.webhookUrl)}`,
440
+ `secret visible: ${app.clientSecret ? "yes" : "no"}`
441
+ ].join(" | ")
442
+ });
443
+ }
444
+ }
445
+ };
446
+ }
447
+ // src/probes/rule-update.ts
448
+ const FROM = 5;
449
+ const TO = 10;
450
+ /** The detector the throwaway rule hangs on: the issue stream of `HOLOCRON_SENTRY_PROBE_PROJECT`, else of any project. */
451
+ async function detectorFor(deps, org) {
452
+ const client = deps.client();
453
+ const slug = createEnvLookup(deps.env).get("HOLOCRON_SENTRY_PROBE_PROJECT");
454
+ const [detectors, projects] = await Promise.all([client.workflows.detectors(org), slug ? client.projects.list(org) : Promise.resolve([])]);
455
+ const streams = detectors.filter((d) => d.type === "issue_stream");
456
+ if (slug) {
457
+ const project = projects.find((p) => p.slug === slug);
458
+ if (!project) throw new Error(`Sentry project \`${slug}\` was not found in ${org}`);
459
+ const own = streams.find((d) => d.projectId === project.id);
460
+ if (!own) throw new Error(`Sentry project \`${slug}\` has no issue stream detector`);
461
+ return own.id;
462
+ }
463
+ const any = streams[0];
464
+ if (!any) throw new Error("no project has an issue stream detector to attach a throwaway rule to");
465
+ return any.id;
466
+ }
467
+ function ruleUpdateProbe(deps) {
468
+ return {
469
+ summary: "Make a disabled throwaway alert rule, change it the way the relay's adopt path does, read it back, delete it.",
470
+ writes: true,
471
+ async run(probe) {
472
+ const org = requireOrg(deps);
473
+ const client = deps.client();
474
+ const detectorId = await detectorFor(deps, org);
475
+ const body = {
476
+ name: `Holocron probe: throwaway rule (${(/* @__PURE__ */ new Date()).toISOString()})`,
477
+ enabled: false,
478
+ environment: null,
479
+ config: { frequency: FROM },
480
+ detectorIds: [detectorId],
481
+ triggers: {
482
+ logicType: "any-short",
483
+ conditions: [{
484
+ type: "new_high_priority_issue",
485
+ comparison: true,
486
+ conditionResult: true
487
+ }]
488
+ },
489
+ actionFilters: [{
490
+ logicType: "all",
491
+ conditions: [],
492
+ actions: []
493
+ }]
494
+ };
495
+ const created = await probe.write("create a disabled throwaway alert rule", async () => {
496
+ const wf = await client.workflows.create(org, body);
497
+ probe.cleanup(`the throwaway rule ${wf.id}`, () => client.workflows.delete(org, wf.id));
498
+ return wf;
499
+ });
500
+ if (!created) {
501
+ probe.say({
502
+ status: "info",
503
+ label: `would then change its frequency ${FROM} to ${TO} with PUT, read it back, and delete it`
504
+ });
505
+ return;
506
+ }
507
+ probe.say({
508
+ status: "ok",
509
+ label: "created",
510
+ detail: `rule ${created.id}, disabled`
511
+ });
512
+ const next = toInput(created);
513
+ next.config = {
514
+ ...created.config,
515
+ frequency: TO
516
+ };
517
+ await client.workflows.update(org, created.id, next);
518
+ probe.say({
519
+ status: "ok",
520
+ label: "PUT accepted"
521
+ });
522
+ const read = await client.workflows.get(org, created.id);
523
+ const kept = !read.enabled && read.detectorIds.includes(detectorId);
524
+ probe.say(read.config.frequency === TO && kept ? {
525
+ status: "ok",
526
+ label: "read back",
527
+ detail: `frequency ${read.config.frequency}, still disabled and attached`
528
+ } : {
529
+ status: "fail",
530
+ label: "read back differs",
531
+ detail: `frequency ${read.config.frequency} (sent ${TO}), enabled ${read.enabled}, detectors ${read.detectorIds.join(",")}`
532
+ });
533
+ }
534
+ };
535
+ }
536
+ // src/probes/rules.ts
537
+ function rulesProbe(deps) {
538
+ return {
539
+ summary: "List the alert rules: enabled, projects attached, and what each action sends to.",
540
+ writes: false,
541
+ async run(probe) {
542
+ const org = requireOrg(deps);
543
+ const client = deps.client();
544
+ const [workflows, apps] = await Promise.all([client.workflows.list(org), client.sentryApps.list(org)]);
545
+ const alertable = new Set(apps.filter((a) => a.isAlertable).map((a) => a.slug));
546
+ if (workflows.length === 0) probe.say({
547
+ status: "info",
548
+ label: "no alert rules"
549
+ });
550
+ for (const wf of workflows) {
551
+ const actions = wf.actionFilters.flatMap((f) => f.actions);
552
+ const described = actions.map((a) => {
553
+ const target = String(a.config["targetIdentifier"] ?? a.config["targetType"] ?? "");
554
+ return target ? `${a.type} ${target}` : a.type;
555
+ });
556
+ const problems = actions.flatMap((a) => {
557
+ if (a.type === "sentry_app") return [`a sentry_app action shows as an unknown integration`];
558
+ if (a.type === "webhook" && !alertable.has(String(a.config["targetIdentifier"]))) return [`webhook ${String(a.config["targetIdentifier"])} is not an alertable integration`];
559
+ return [];
560
+ });
561
+ probe.say({
562
+ status: problems.length > 0 || !wf.enabled ? "fail" : "ok",
563
+ label: wf.name,
564
+ detail: [
565
+ wf.enabled ? "enabled" : "DISABLED",
566
+ `detectors: ${wf.detectorIds.length}`,
567
+ `actions: ${described.join(", ") || "none"}`,
568
+ ...problems
569
+ ].join(" | ")
570
+ });
571
+ }
572
+ }
573
+ };
574
+ }
575
+ // src/probes/send-error.ts
576
+ /** Which DSN to send to: `HOLOCRON_SENTRY_DSN`, else `SENTRY_DSN`, else the first client key of the project named in `HOLOCRON_SENTRY_PROBE_PROJECT`. */
577
+ async function dsnFor(deps) {
578
+ const env = holocronEnv(deps.env);
579
+ const configured = env.get("SENTRY_DSN");
580
+ if (configured) return configured;
581
+ const project = env.get("HOLOCRON_SENTRY_PROBE_PROJECT");
582
+ if (!project) throw new Error("set SENTRY_DSN, or HOLOCRON_SENTRY_PROBE_PROJECT to a project slug, to say where to send the error");
583
+ const dsn = (await deps.client().projects.keys(requireOrg(deps), project))[0]?.dsn.public;
584
+ if (!dsn) throw new Error(`project \`${project}\` has no client key`);
585
+ return dsn;
586
+ }
587
+ function sendErrorProbe(deps) {
588
+ return {
589
+ summary: "Send one labelled test error to a project, to follow it through the alert rule to Discord.",
590
+ writes: true,
591
+ async run(probe) {
592
+ const dsn = new URL(await dsnFor(deps));
593
+ probe.secret(dsn.username);
594
+ const projectId = dsn.pathname.replace(/^\/+/, "");
595
+ const eventId = randomUUID().replaceAll("-", "");
596
+ const message = `Holocron probe: sent on purpose (${(/* @__PURE__ */ new Date()).toISOString()})`;
597
+ const envelope = [
598
+ JSON.stringify({
599
+ event_id: eventId,
600
+ sent_at: (/* @__PURE__ */ new Date()).toISOString()
601
+ }),
602
+ JSON.stringify({ type: "event" }),
603
+ JSON.stringify({
604
+ event_id: eventId,
605
+ level: "error",
606
+ platform: "node",
607
+ environment: "holocron-probe",
608
+ message: { formatted: message },
609
+ exception: { values: [{
610
+ type: "HolocronProbeError",
611
+ value: message
612
+ }] },
613
+ tags: { probe: "sentry.send-error" }
614
+ })
615
+ ].join("\n");
616
+ if (await probe.write("send a test error", async () => {
617
+ const res = await (deps.fetch ?? fetch)(`${dsn.protocol}//${dsn.host}/api/${projectId}/envelope/?sentry_version=7&sentry_key=${dsn.username}&sentry_client=holocron-probe/1`, {
618
+ method: "POST",
619
+ headers: { "content-type": "application/x-sentry-envelope" },
620
+ body: envelope
621
+ });
622
+ if (!res.ok) throw new Error(`Sentry answered ${res.status}`);
623
+ return true;
624
+ })) probe.say({
625
+ status: "ok",
626
+ label: "sent HolocronProbeError",
627
+ detail: "look for it in the project's issues and, through the rule, in #errors; resolve the issue by hand (a probe cannot delete it)"
628
+ });
629
+ }
630
+ };
631
+ }
632
+ // src/probes/index.ts
633
+ function sentryProbes(deps) {
634
+ return {
635
+ integrations: integrationsProbe(deps),
636
+ rules: rulesProbe(deps),
637
+ "rule-update": ruleUpdateProbe(deps),
638
+ "send-error": sendErrorProbe(deps)
639
+ };
640
+ }
641
+ // src/probes/token/parse-token-args.ts
642
+ /** Reads `--layer <word>` and `--token-name <VAR>` (either spelling). Anything else, and a flag with no value, is ignored. */
643
+ function parseTokenArgs(args) {
644
+ let layer;
645
+ let tokenName;
646
+ for (let i = 0; i < args.length; i++) {
647
+ const arg = args[i];
648
+ if (arg === "--layer") layer = args[++i];
649
+ else if (arg.startsWith("--layer=")) layer = arg.slice(8);
650
+ else if (arg === "--token-name") tokenName = args[++i];
651
+ else if (arg.startsWith("--token-name=")) tokenName = arg.slice(13);
652
+ }
653
+ return {
654
+ ...layer ? { layer } : {},
655
+ ...tokenName ? { tokenName } : {}
656
+ };
657
+ }
658
+ // src/probes/token/token.ts
659
+ /** How many names to show before "(+N more)". */
660
+ const SHOWN = 5;
661
+ /** `a, b, c (+2 more)`. */
662
+ function summarise(names) {
663
+ return names.length <= SHOWN ? names.join(", ") : `${names.slice(0, SHOWN).join(", ")} (+${names.length - SHOWN} more)`;
664
+ }
665
+ /**
666
+ * Reads one list and says what came back. Sentry answers a token without the scope with a 401 or 403, and an empty
667
+ * list may only mean there are none, so an empty list is `none` too. Any other failure (the network, a 404, a 5xx) is an
668
+ * `error`.
669
+ */
670
+ async function reach(read, noun) {
671
+ try {
672
+ const items = await read();
673
+ if (items.length === 0) return {
674
+ kind: "none",
675
+ detail: `no ${noun} visible (none exist, or the token cannot see them)`
676
+ };
677
+ const names = items.map((i) => i.slug ?? i.name).filter((n) => typeof n === "string");
678
+ return {
679
+ kind: "has",
680
+ detail: names.length > 0 ? `${items.length} ${noun}: ${summarise(names)}` : `${items.length} ${noun}`
681
+ };
682
+ } catch (err) {
683
+ if (err instanceof ProviderApiError && (err.status === 401 || err.status === 403)) return {
684
+ kind: "none",
685
+ detail: `no access (${err.status})`
686
+ };
687
+ return {
688
+ kind: "error",
689
+ detail: errorMessage(err)
690
+ };
691
+ }
692
+ }
693
+ /**
694
+ * One row for what was tried. With no rule it only describes (`ok` for access, `info` for none). Under a layer's rule it
695
+ * judges: a required permission the token lacks fails, and one the layer must not have fails when the token has it; a
696
+ * permission that is rightly absent is a bare `ok`.
697
+ */
698
+ function rowFor({ label, outcome }, rule) {
699
+ if (outcome.kind === "error") return {
700
+ status: "fail",
701
+ label,
702
+ detail: outcome.detail
703
+ };
704
+ const has = outcome.kind === "has";
705
+ if (rule === "required") return has ? {
706
+ status: "ok",
707
+ label,
708
+ detail: outcome.detail
709
+ } : {
710
+ status: "fail",
711
+ label,
712
+ detail: outcome.detail
713
+ };
714
+ if (rule === "absent") return has ? {
715
+ status: "fail",
716
+ label,
717
+ detail: outcome.detail
718
+ } : {
719
+ status: "ok",
720
+ label
721
+ };
722
+ return {
723
+ status: has ? "ok" : "info",
724
+ label,
725
+ detail: outcome.detail
726
+ };
727
+ }
728
+ /**
729
+ * The `token` probe: reads what Holocron reads from Sentry (the organizations, the projects and the alert rules) with a
730
+ * token and reports which of them it reaches. It only reads, so it changes nothing. The token is the one the plugin
731
+ * resolves, so `--token sentry=<token>` or `--environment <layer>` checks another one; the organization is the
732
+ * provider's `org` option. A write scope (`project:write`, `org:write`, `alerts:write`) has no read that proves it, so it
733
+ * is not probed.
734
+ *
735
+ * A token with no layer is only described. When the CLI passes `--layer` (because `--environment` named `ci`, `dev` or
736
+ * `local`), the results are judged against what that layer's token is meant to carry (the plugin's `tokens`
737
+ * declaration, read with `tokenExpectations()` and narrowed to the token `--token-name` names, which the CLI passes for
738
+ * each declared token the Environment holds) and shown in two groups, what it should have and what it should not, where
739
+ * a missing or extra permission fails.
740
+ */
741
+ function tokenProbe(ctx) {
742
+ return {
743
+ summary: "Try each Sentry read Holocron uses (organizations, projects, alert rules) with a token and report which it reaches; for a ci or dev Environment, check it against what that layer should carry. Read-only.",
744
+ writes: false,
745
+ acceptsToken: true,
746
+ async run(probe) {
747
+ const { layer, tokenName } = parseTokenArgs(probe.args ?? []);
748
+ const token = ctx.token();
749
+ probe.secret(token);
750
+ const client = ctx.client();
751
+ const rules = layer ? tokenExpectations(tokens, {
752
+ layer,
753
+ ...tokenName ? { envName: tokenName } : {}
754
+ })[0]?.rules : void 0;
755
+ if (layer && !rules) probe.say({
756
+ status: "info",
757
+ label: layer,
758
+ detail: "no expectations are declared for it, so the token is only described"
759
+ });
760
+ const tried = [{
761
+ label: "Organizations: Read",
762
+ permission: "org",
763
+ outcome: await reach(() => client.auth.organizations(), "organizations")
764
+ }];
765
+ const org = ctx.org;
766
+ if (org) {
767
+ tried.push({
768
+ label: "Projects: Read",
769
+ permission: "projects",
770
+ outcome: await reach(() => client.projects.list(org), "projects")
771
+ });
772
+ tried.push({
773
+ label: "Alert rules: Read",
774
+ permission: "alerts",
775
+ outcome: await reach(() => client.workflows.list(org), "alert rules")
776
+ });
777
+ } else probe.say({
778
+ status: rules ? "fail" : "info",
779
+ label: "projects and alert rules",
780
+ detail: "no org: set `org` in the sentry provider's options to check them"
781
+ });
782
+ if (!rules) {
783
+ for (const t of tried) probe.say(rowFor(t, void 0));
784
+ return;
785
+ }
786
+ for (const [heading, rule] of [["Should have", "required"], ["Should not have", "absent"]]) {
787
+ const inGroup = tried.filter((t) => rules[t.permission] === rule);
788
+ if (inGroup.length === 0) continue;
789
+ probe.say({
790
+ status: "heading",
791
+ label: heading
792
+ });
793
+ for (const t of inGroup) probe.say(rowFor(t, rule));
794
+ }
795
+ }
796
+ };
797
+ }
798
+ // src/verify-token.ts
799
+ /** Checks a Sentry token by listing its organizations; the success subject is `org:` and the first organization's slug, the failure carries the error message. */
73
800
  async function verifyToken(token, opts = {}) {
74
801
  const client = createSentryClient({
75
802
  token,
@@ -84,36 +811,66 @@ async function verifyToken(token, opts = {}) {
84
811
  } catch (err) {
85
812
  return {
86
813
  ok: false,
87
- message: err instanceof Error ? err.message : String(err)
814
+ message: errorMessage(err)
88
815
  };
89
816
  }
90
817
  }
91
- //#endregion
92
- //#region src/index.ts
818
+ // src/index.ts
819
+ /** Creates the shared context for the given options; no token is resolved until a client is first called. */
93
820
  function createContext(options) {
94
821
  let client;
822
+ let relayClient;
95
823
  return {
96
824
  options,
97
825
  client: () => client ??= createSentryClient({
98
826
  token: resolveToken(options),
99
827
  baseUrl: options.baseUrl,
100
828
  fetch: options.fetch
829
+ }),
830
+ relayClient: () => relayClient ??= createSentryClient({
831
+ token: resolveRelayToken(options),
832
+ baseUrl: options.baseUrl,
833
+ fetch: options.fetch
101
834
  })
102
835
  };
103
836
  }
837
+ /** Builds the `errors` capability from a plugin context, using the relay client for the Comlink relay. */
104
838
  function errors(ctx) {
105
839
  return new SentryErrors(ctx.client, {
106
840
  org: ctx.options.org,
107
841
  team: ctx.options.team
108
- });
842
+ }, ctx.relayClient);
109
843
  }
844
+ /** Creates the Sentry plugin: its probes, a `connection` probe and the `errors` capability. */
110
845
  function createPlugin(options) {
111
846
  const ctx = createContext(options);
112
847
  return {
113
848
  name: "@theholocron/holocron-plugin-sentry",
849
+ probes: {
850
+ ...sentryProbes({
851
+ org: options.org,
852
+ client: ctx.relayClient,
853
+ fetch: options.fetch
854
+ }),
855
+ token: tokenProbe({
856
+ org: options.org,
857
+ client: ctx.client,
858
+ token: () => resolveToken(options)
859
+ }),
860
+ connection: connectionProbe({
861
+ provider: "Sentry",
862
+ url: options.baseUrl ?? "https://sentry.io",
863
+ fetch: options.fetch,
864
+ token: () => resolveToken(options),
865
+ verify: (token) => verifyToken(token, {
866
+ baseUrl: options.baseUrl,
867
+ fetch: options.fetch
868
+ })
869
+ })
870
+ },
114
871
  capabilities: { errors: () => errors(ctx) }
115
872
  };
116
873
  }
117
- const AUTH_HINT = "generate an auth token at https://sentry.io/settings/account/api/auth-tokens/ with project:read, project:write, and org:read scopes, then run: holocron auth set sentry <TOKEN>";
118
- //#endregion
119
- export { AUTH_HINT, AuthError, SentryErrors, createContext, createPlugin, createSentryClient, errors, resolveToken, verifyToken };
874
+ /** One-line hint shown by `holocron new token sentry` when no token is given or the token is rejected. */
875
+ const AUTH_HINT = "generate an auth token at https://sentry.io/settings/account/api/auth-tokens/ with project:read, project:write, and org:read scopes, then run: holocron new token sentry <TOKEN>";
876
+ export { AUTH_HINT, AuthError, SentryErrors, createContext, createPlugin, createSentryClient, errors, resolveComlinkToken, resolveRelayToken, resolveToken, tokens, verifyToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-sentry",
3
- "version": "5.0.0-alpha.44",
3
+ "version": "5.0.0-alpha.442",
4
4
  "description": "Holocron plugin for Sentry. Implements the errors capability against Sentry's management API — project provisioning and DSN retrieval.",
5
5
  "keywords": [
6
6
  "errors",
@@ -8,12 +8,12 @@
8
8
  "plugin",
9
9
  "sentry"
10
10
  ],
11
- "homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-sentry#readme",
11
+ "homepage": "https://docs.theholocron.dev/holocron/plugins/sentry/",
12
12
  "bugs": "https://github.com/theholocron/holocron/issues",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git+https://github.com/theholocron/holocron.git",
16
- "directory": "packages/holocron-plugin-sentry"
16
+ "directory": "plugins/holocron-plugin-sentry"
17
17
  },
18
18
  "license": "MIT",
19
19
  "author": "Newton Koumantzelis",
@@ -29,29 +29,34 @@
29
29
  "files": [
30
30
  "dist"
31
31
  ],
32
+ "dependencies": {
33
+ "@theholocron/env-utils": "^1.17.0-alpha.4"
34
+ },
32
35
  "devDependencies": {
33
- "@theholocron/eslint-config": "^8.4.5",
34
- "@theholocron/http-client": "^1.25.0",
35
- "@theholocron/sentry-client": "^1.25.0",
36
- "@theholocron/tsconfig": "^8.4.5",
37
- "@theholocron/tsdown-config": "^8.4.5",
38
- "@theholocron/vitest-config": "^8.4.5",
39
36
  "@types/node": "^26",
40
- "@vitest/coverage-v8": "^4.1.11",
41
- "@vitest/eslint-plugin": "^1.6.27",
42
- "chalk": "^6.0.0",
43
- "eslint": "^10.8.1",
44
- "eslint-plugin-n": "^18.3.0",
45
- "globals": "^17.11.0",
46
- "tsdown": "^0.22.14",
47
- "tsx": "4.23.12",
37
+ "@vitest/coverage-v8": "^5.1.0",
38
+ "@vitest/eslint-plugin": "^1.6.28",
39
+ "chalk": "^6.0.1",
40
+ "eslint": "^10.14.0",
41
+ "eslint-plugin-n": "^18.5.0",
42
+ "globals": "^17.14.0",
43
+ "tsdown": "^0.23.5",
44
+ "tsx": "4.23.15",
48
45
  "typescript": "^5.9.3",
49
- "vitest": "^4.1.11",
50
- "@theholocron/cli": "5.0.0-alpha.44"
46
+ "vitest": "^5.1.0",
47
+ "@theholocron/cli": "5.0.0-alpha.442",
48
+ "@theholocron/eslint-config": "^8.9.0-alpha.17",
49
+ "@theholocron/http-client": "^1.58.0-alpha.32",
50
+ "@theholocron/misc-utils": "^1.17.0-alpha.4",
51
+ "@theholocron/sentry-client": "^1.58.0-alpha.32",
52
+ "@theholocron/tsconfig": "^8.9.0-alpha.17",
53
+ "@theholocron/tsdown-config": "^8.9.0-alpha.17",
54
+ "@theholocron/vitest-config": "^8.9.0-alpha.17"
51
55
  },
52
56
  "peerDependencies": {
53
- "@theholocron/sentry-client": "^1.25.0",
54
- "@theholocron/cli": "5.0.0-alpha.44"
57
+ "@theholocron/misc-utils": "^1.16.0",
58
+ "@theholocron/sentry-client": "^1.56.0",
59
+ "@theholocron/cli": "5.0.0-alpha.442"
55
60
  },
56
61
  "peerDependenciesMeta": {
57
62
  "@theholocron/sentry-client": {}
@@ -63,11 +68,11 @@
63
68
  "access": "public"
64
69
  },
65
70
  "scripts": {
66
- "delivery.build": "tsdown",
67
- "sourceQuality.staticAnalysis": "holocron run sourceQuality.staticAnalysis --",
71
+ "build": "tsdown",
72
+ "lint:code": "eslint .",
73
+ "test:unit": "vitest run --coverage",
68
74
  "test:watch": "vitest",
69
- "validate": "tsx scripts/validate.mjs",
70
- "verification.typeSafety": "holocron run verification.typeSafety --",
71
- "verification.unitTests": "holocron run verification.unitTests --"
75
+ "typecheck": "tsc --noEmit",
76
+ "validate:plugin": "tsx scripts/validate.mjs"
72
77
  }
73
78
  }