@nacre.work/api 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/messages.js CHANGED
@@ -1,4 +1,4 @@
1
- import { consoleUrl, message } from '@nacre.work/core';
1
+ import { consoleUrl, message, notificationMessage, ruleSentence } from '@nacre.work/core';
2
2
  /**
3
3
  * Every message this product sends, in one file.
4
4
  *
@@ -99,6 +99,11 @@ export function secondFactorMessage(to, what) {
99
99
  * All of them, for anything that has to reason about the set rather than about
100
100
  * one — the preview, and whatever comes after it.
101
101
  *
102
+ * Including the notification, which is written in the core because the worker
103
+ * sends it and cannot import this package. Listed here all the same, in both
104
+ * of its forms: the set is what the preview renders, and a message missing
105
+ * from it is a message nobody looked at in a mail client.
106
+ *
102
107
  * A list rather than a count, because a count is the thing that goes stale.
103
108
  */
104
109
  export const everyMessage = (to, consoleBase) => [
@@ -107,5 +112,19 @@ export const everyMessage = (to, consoleBase) => [
107
112
  passwordChangedMessage(to, 'recovery-link'),
108
113
  secondFactorMessage(to, 'enrolled'),
109
114
  secondFactorMessage(to, 'removed'),
115
+ notificationMessage(to, consoleBase, {
116
+ subject: 'Contracts layer reorganised',
117
+ body: 'The contracts layer now holds signed agreements only. Drafts moved to contracts-drafts.\n\n' +
118
+ 'Grants on contracts are unchanged; ask an administrator if you need the drafts.',
119
+ }, { source: 'agent', appliedBy: 'dana@example.com', application: 'Claude' }),
120
+ notificationMessage(to, consoleBase, {
121
+ subject: '12 documents failed to index',
122
+ body: '12 documents failed to index in the last 60 minutes:\n\n- handbook: 9\n- contracts: 3\n\n' +
123
+ 'Each failed document carries its reason.',
124
+ }, {
125
+ source: 'rule',
126
+ when: ruleSentence({ kind: 'documents_failed', layerSlug: null, threshold: 10, windowMinutes: 60 }),
127
+ ruleBy: 'dana@example.com',
128
+ }),
110
129
  ];
111
130
  //# sourceMappingURL=messages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAgB,MAAM,kBAAkB,CAAA;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,+DAA+D;AAC/D,MAAM,UAAU,oBAAoB,CAClC,EAAU,EACV,WAAmB,EACnB,KAAa,EACb,UAAkB;IAElB,MAAM,IAAI,GAAG,UAAU,CAAC,WAAW,EAAE,iBAAiB,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAClF,OAAO,OAAO,CAAC,EAAE,EAAE,2BAA2B,EAAE;QAC9C,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,wDAAwD,EAAE;QAC/E,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,qBAAqB,EAAE;QACzD;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EACF,sCAAsC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC,YAAY;gBACrF,oFAAoF;gBACpF,oBAAoB;SACvB;QACD,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,yEAAyE,EAAE;KACjG,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,EAAU,EAAE,GAAgC;IACjF,OAAO,OAAO,CAAC,EAAE,EAAE,iCAAiC,EAAE;QACpD;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EACF,GAAG,KAAK,SAAS;gBACf,CAAC,CAAC,0EAA0E;gBAC5E,CAAC,CAAC,4EAA4E;SACnF;QACD;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EACF,oFAAoF;gBACpF,wBAAwB;SAC3B;QACD;YACE,IAAI,EAAE,SAAS;YACf,IAAI,EACF,GAAG,KAAK,SAAS;gBACf,CAAC,CAAC,+EAA+E;oBAC/E,qCAAqC;gBACvC,CAAC,CAAC,+EAA+E;oBAC/E,4DAA4D;SACnE;KACF,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAU,EAAE,IAA4B;IAC1E,OAAO,IAAI,KAAK,UAAU;QACxB,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,iDAAiD,EAAE;YAC7D,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,kDAAkD,EAAE;YACzE;gBACE,IAAI,EAAE,SAAS;gBACf,IAAI,EACF,kFAAkF;oBAClF,qBAAqB;aACxB;SACF,CAAC;QACJ,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,qDAAqD,EAAE;YACjE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,sDAAsD,EAAE;YAC7E;gBACE,IAAI,EAAE,SAAS;gBACf,IAAI,EACF,iFAAiF;oBACjF,4DAA4D;aAC/D;SACF,CAAC,CAAA;AACR,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAAU,EAAE,WAAmB,EAAsB,EAAE,CAAC;IACnF,oBAAoB,CAAC,EAAE,EAAE,WAAW,EAAE,0CAA0C,EAAE,IAAI,CAAC;IACvF,sBAAsB,CAAC,EAAE,EAAE,SAAS,CAAC;IACrC,sBAAsB,CAAC,EAAE,EAAE,eAAe,CAAC;IAC3C,mBAAmB,CAAC,EAAE,EAAE,UAAU,CAAC;IACnC,mBAAmB,CAAC,EAAE,EAAE,SAAS,CAAC;CACnC,CAAA"}
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAgB,MAAM,kBAAkB,CAAA;AAEvG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,+DAA+D;AAC/D,MAAM,UAAU,oBAAoB,CAClC,EAAU,EACV,WAAmB,EACnB,KAAa,EACb,UAAkB;IAElB,MAAM,IAAI,GAAG,UAAU,CAAC,WAAW,EAAE,iBAAiB,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAClF,OAAO,OAAO,CAAC,EAAE,EAAE,2BAA2B,EAAE;QAC9C,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,wDAAwD,EAAE;QAC/E,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,qBAAqB,EAAE;QACzD;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EACF,sCAAsC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC,YAAY;gBACrF,oFAAoF;gBACpF,oBAAoB;SACvB;QACD,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,yEAAyE,EAAE;KACjG,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,EAAU,EAAE,GAAgC;IACjF,OAAO,OAAO,CAAC,EAAE,EAAE,iCAAiC,EAAE;QACpD;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EACF,GAAG,KAAK,SAAS;gBACf,CAAC,CAAC,0EAA0E;gBAC5E,CAAC,CAAC,4EAA4E;SACnF;QACD;YACE,IAAI,EAAE,KAAK;YACX,IAAI,EACF,oFAAoF;gBACpF,wBAAwB;SAC3B;QACD;YACE,IAAI,EAAE,SAAS;YACf,IAAI,EACF,GAAG,KAAK,SAAS;gBACf,CAAC,CAAC,+EAA+E;oBAC/E,qCAAqC;gBACvC,CAAC,CAAC,+EAA+E;oBAC/E,4DAA4D;SACnE;KACF,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAU,EAAE,IAA4B;IAC1E,OAAO,IAAI,KAAK,UAAU;QACxB,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,iDAAiD,EAAE;YAC7D,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,kDAAkD,EAAE;YACzE;gBACE,IAAI,EAAE,SAAS;gBACf,IAAI,EACF,kFAAkF;oBAClF,qBAAqB;aACxB;SACF,CAAC;QACJ,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,qDAAqD,EAAE;YACjE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,sDAAsD,EAAE;YAC7E;gBACE,IAAI,EAAE,SAAS;gBACf,IAAI,EACF,iFAAiF;oBACjF,4DAA4D;aAC/D;SACF,CAAC,CAAA;AACR,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAAU,EAAE,WAAmB,EAAsB,EAAE,CAAC;IACnF,oBAAoB,CAAC,EAAE,EAAE,WAAW,EAAE,0CAA0C,EAAE,IAAI,CAAC;IACvF,sBAAsB,CAAC,EAAE,EAAE,SAAS,CAAC;IACrC,sBAAsB,CAAC,EAAE,EAAE,eAAe,CAAC;IAC3C,mBAAmB,CAAC,EAAE,EAAE,UAAU,CAAC;IACnC,mBAAmB,CAAC,EAAE,EAAE,SAAS,CAAC;IAClC,mBAAmB,CACjB,EAAE,EACF,WAAW,EACX;QACE,OAAO,EAAE,6BAA6B;QACtC,IAAI,EACF,6FAA6F;YAC7F,iFAAiF;KACpF,EACD,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,WAAW,EAAE,QAAQ,EAAE,CAC1E;IACD,mBAAmB,CACjB,EAAE,EACF,WAAW,EACX;QACE,OAAO,EAAE,8BAA8B;QACvC,IAAI,EACF,2FAA2F;YAC3F,0CAA0C;KAC7C,EACD;QACE,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,YAAY,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC;QACnG,MAAM,EAAE,kBAAkB;KAC3B,CACF;CACF,CAAA"}
@@ -0,0 +1,108 @@
1
+ import { type AlertKind } from '@nacre.work/core';
2
+ import type { Pool } from 'pg';
3
+ import type { AuthContext } from './auth.js';
4
+ /**
5
+ * The outbox and the alert rules, from the side that proposes and applies.
6
+ * docs/mcp-admin.md, "Notifications".
7
+ *
8
+ * Nothing here sends: the worker does, reading addresses from `users` at the
9
+ * moment it sends. This side's job is the first of the three bounds on who can
10
+ * receive one — that every recipient it stores is an **active user of the
11
+ * caller's organization**, resolved here, inside `withOrg`, with the
12
+ * organization named in each statement's own literal.
13
+ */
14
+ /** A rule as an administrator reads it back. */
15
+ export interface AlertRule {
16
+ readonly id: string;
17
+ readonly kind: AlertKind;
18
+ readonly layerId: string | null;
19
+ readonly layerSlug: string | null;
20
+ readonly threshold: number | null;
21
+ readonly windowMinutes: number | null;
22
+ readonly recipients: readonly string[];
23
+ readonly toOrgAdmins: boolean;
24
+ readonly createdBy: string;
25
+ readonly createdAt: string;
26
+ readonly lastFiredAt: string | null;
27
+ }
28
+ export interface NewAlertRule {
29
+ readonly kind: AlertKind;
30
+ readonly layerId: string | null;
31
+ readonly threshold: number | null;
32
+ readonly windowMinutes: number | null;
33
+ readonly recipients: readonly string[];
34
+ readonly toOrgAdmins: boolean;
35
+ }
36
+ export type Enqueued = {
37
+ readonly kind: 'queued';
38
+ readonly id: string;
39
+ } | {
40
+ readonly kind: 'limited';
41
+ };
42
+ /** Active rules one organization may keep. A rule is a standing query against its log every minute. */
43
+ export declare const MAX_ALERT_RULES = 50;
44
+ export interface NotificationStore {
45
+ /**
46
+ * Each reference — an address or an id — resolved to an active person in the
47
+ * caller's organization, or listed as unknown. Unknown is everything else:
48
+ * another organization's person, a disabled one, a service account's name,
49
+ * an address nobody has. One answer for all of them, the way a `404` is.
50
+ */
51
+ people(auth: AuthContext, refs: readonly string[]): Promise<{
52
+ readonly found: ReadonlyMap<string, {
53
+ readonly id: string;
54
+ readonly email: string;
55
+ }>;
56
+ readonly unknown: readonly string[];
57
+ }>;
58
+ /** How many active organization administrators there are: what "every org_admin" reaches today. */
59
+ orgAdmins(auth: AuthContext): Promise<number>;
60
+ /** Into the outbox, inside the hourly bound. */
61
+ enqueue(auth: AuthContext, notification: {
62
+ readonly recipients: readonly string[];
63
+ readonly toOrgAdmins: boolean;
64
+ readonly subject: string;
65
+ readonly body: string;
66
+ readonly consentId: string;
67
+ }): Promise<Enqueued>;
68
+ rules(auth: AuthContext): Promise<readonly AlertRule[]>;
69
+ createRule(auth: AuthContext, rule: NewAlertRule): Promise<{
70
+ readonly kind: 'created';
71
+ readonly id: string;
72
+ } | {
73
+ readonly kind: 'full';
74
+ }>;
75
+ removeRule(auth: AuthContext, id: string): Promise<boolean>;
76
+ rule(auth: AuthContext, id: string): Promise<AlertRule | undefined>;
77
+ }
78
+ export declare class PostgresNotifications implements NotificationStore {
79
+ private readonly pool;
80
+ private readonly role;
81
+ constructor(pool: Pool, role: string);
82
+ private inOrg;
83
+ people(auth: AuthContext, refs: readonly string[]): Promise<{
84
+ found: ReadonlyMap<string, {
85
+ id: string;
86
+ email: string;
87
+ }>;
88
+ unknown: readonly string[];
89
+ }>;
90
+ orgAdmins(auth: AuthContext): Promise<number>;
91
+ enqueue(auth: AuthContext, n: {
92
+ recipients: readonly string[];
93
+ toOrgAdmins: boolean;
94
+ subject: string;
95
+ body: string;
96
+ consentId: string;
97
+ }): Promise<Enqueued>;
98
+ rules(auth: AuthContext): Promise<readonly AlertRule[]>;
99
+ rule(auth: AuthContext, id: string): Promise<AlertRule | undefined>;
100
+ createRule(auth: AuthContext, rule: NewAlertRule): Promise<{
101
+ kind: 'created';
102
+ id: string;
103
+ } | {
104
+ kind: 'full';
105
+ }>;
106
+ removeRule(auth: AuthContext, id: string): Promise<boolean>;
107
+ }
108
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ import { NOTIFY_PER_HOUR, withOrg } from '@nacre.work/core';
2
+ import { isUuid } from './admin-names.js';
3
+ /** Active rules one organization may keep. A rule is a standing query against its log every minute. */
4
+ export const MAX_ALERT_RULES = 50;
5
+ const RULE_COLUMNS = `r.id, r.kind, r.layer_id, l.slug AS layer_slug, r.threshold, r.window_minutes, r.recipients,
6
+ r.to_org_admins, r.created_by, r.created_at, r.last_fired_at`;
7
+ const ruleOf = (r) => ({
8
+ id: r.id,
9
+ kind: r.kind,
10
+ layerId: r.layer_id,
11
+ layerSlug: r.layer_slug,
12
+ threshold: r.threshold,
13
+ windowMinutes: r.window_minutes,
14
+ recipients: r.recipients,
15
+ toOrgAdmins: r.to_org_admins,
16
+ createdBy: r.created_by,
17
+ createdAt: r.created_at.toISOString(),
18
+ lastFiredAt: r.last_fired_at === null ? null : r.last_fired_at.toISOString(),
19
+ });
20
+ export class PostgresNotifications {
21
+ pool;
22
+ role;
23
+ constructor(pool, role) {
24
+ this.pool = pool;
25
+ this.role = role;
26
+ }
27
+ inOrg(auth, run) {
28
+ return withOrg(this.pool, auth.orgId, run, { role: this.role });
29
+ }
30
+ people(auth, refs) {
31
+ return this.inOrg(auth, async (client) => {
32
+ const wanted = [...new Set(refs.map((r) => r.trim()).filter((r) => r !== ''))];
33
+ const ids = wanted.filter(isUuid);
34
+ const emails = wanted.filter((r) => !isUuid(r)).map((r) => r.toLowerCase());
35
+ // Both shapes in one statement, and the organization in its own literal:
36
+ // RLS is the second line here, not the mechanism.
37
+ const { rows } = await client.query(`SELECT id, email FROM users
38
+ WHERE org_id = $1 AND disabled_at IS NULL
39
+ AND (id = ANY($2::uuid[]) OR lower(email) = ANY($3::text[]))`, [auth.orgId, ids, emails]);
40
+ const found = new Map();
41
+ const unknown = [];
42
+ for (const ref of wanted) {
43
+ const hit = rows.filter((r) => (isUuid(ref) ? r.id === ref.toLowerCase() : r.email.toLowerCase() === ref.toLowerCase()));
44
+ // An address two accounts share would be two people; refused as unknown
45
+ // rather than guessed between. `users` is unique on it, so this is the
46
+ // shape the code has rather than one the data can produce today.
47
+ if (hit.length === 1 && hit[0] !== undefined)
48
+ found.set(ref, hit[0]);
49
+ else
50
+ unknown.push(ref);
51
+ }
52
+ return { found, unknown };
53
+ });
54
+ }
55
+ orgAdmins(auth) {
56
+ return this.inOrg(auth, async (client) => {
57
+ const { rows } = await client.query(`SELECT count(*)::int AS n FROM users WHERE org_id = $1 AND role = 'org_admin' AND disabled_at IS NULL`, [auth.orgId]);
58
+ return rows[0]?.n ?? 0;
59
+ });
60
+ }
61
+ enqueue(auth, n) {
62
+ return this.inOrg(auth, async (client) => {
63
+ // One organization's count at a time, so two applies in the same second
64
+ // cannot both read twenty-nine. The lock is the transaction's and goes
65
+ // with it.
66
+ await client.query(`SELECT pg_advisory_xact_lock(hashtextextended('nacre.notify:' || $1::text, 0))`, [auth.orgId]);
67
+ const { rows } = await client.query(`INSERT INTO notifications (org_id, recipients, to_org_admins, subject, body, source, sent_by, consent_id)
68
+ SELECT $1, $2::uuid[], $3, $4, $5, 'agent', $6, $7
69
+ WHERE (SELECT count(*) FROM notifications
70
+ WHERE org_id = $1 AND created_at > now() - interval '1 hour') < $8
71
+ RETURNING id`, [auth.orgId, n.recipients, n.toOrgAdmins, n.subject, n.body, auth.principal.id, n.consentId, NOTIFY_PER_HOUR]);
72
+ const id = rows[0]?.id;
73
+ return id === undefined ? { kind: 'limited' } : { kind: 'queued', id };
74
+ });
75
+ }
76
+ rules(auth) {
77
+ return this.inOrg(auth, async (client) => {
78
+ const { rows } = await client.query(`SELECT ${RULE_COLUMNS}
79
+ FROM alert_rules r LEFT JOIN layers l ON l.id = r.layer_id AND l.org_id = r.org_id
80
+ WHERE r.org_id = $1 AND r.removed_at IS NULL
81
+ ORDER BY r.created_at, r.id`, [auth.orgId]);
82
+ return rows.map(ruleOf);
83
+ });
84
+ }
85
+ rule(auth, id) {
86
+ if (!isUuid(id))
87
+ return Promise.resolve(undefined);
88
+ return this.inOrg(auth, async (client) => {
89
+ const { rows } = await client.query(`SELECT ${RULE_COLUMNS}
90
+ FROM alert_rules r LEFT JOIN layers l ON l.id = r.layer_id AND l.org_id = r.org_id
91
+ WHERE r.org_id = $1 AND r.id = $2::uuid AND r.removed_at IS NULL`, [auth.orgId, id]);
92
+ return rows[0] === undefined ? undefined : ruleOf(rows[0]);
93
+ });
94
+ }
95
+ createRule(auth, rule) {
96
+ return this.inOrg(auth, async (client) => {
97
+ await client.query(`SELECT pg_advisory_xact_lock(hashtextextended('nacre.alert-rules:' || $1::text, 0))`, [auth.orgId]);
98
+ const { rows } = await client.query(`INSERT INTO alert_rules (org_id, kind, layer_id, threshold, window_minutes, recipients, to_org_admins, created_by)
99
+ SELECT $1, $2, $3, $4, $5, $6::uuid[], $7, $8
100
+ WHERE (SELECT count(*) FROM alert_rules WHERE org_id = $1 AND removed_at IS NULL) < $9
101
+ RETURNING id`, [
102
+ auth.orgId,
103
+ rule.kind,
104
+ rule.layerId,
105
+ rule.threshold,
106
+ rule.windowMinutes,
107
+ rule.recipients,
108
+ rule.toOrgAdmins,
109
+ auth.principal.id,
110
+ MAX_ALERT_RULES,
111
+ ]);
112
+ const id = rows[0]?.id;
113
+ return id === undefined ? { kind: 'full' } : { kind: 'created', id };
114
+ });
115
+ }
116
+ removeRule(auth, id) {
117
+ if (!isUuid(id))
118
+ return Promise.resolve(false);
119
+ return this.inOrg(auth, async (client) => {
120
+ const { rowCount } = await client.query(`UPDATE alert_rules SET removed_at = now() WHERE org_id = $1 AND id = $2::uuid AND removed_at IS NULL`, [auth.orgId, id]);
121
+ return rowCount === 1;
122
+ });
123
+ }
124
+ }
125
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -4570,6 +4570,10 @@ async function handle(req, res, options) {
4570
4570
  detail: {
4571
4571
  client_id: clientId,
4572
4572
  client_name: client.clientName,
4573
+ // Which resource the connection is for. The `admin_connection` alert
4574
+ // rule reads this, and "when was an administrative connection
4575
+ // approved" is the question it answers for a person too.
4576
+ surface: administrative ? 'admin' : 'default',
4573
4577
  acts_as: subject.actsAs,
4574
4578
  ...(delegating
4575
4579
  ? {