@intelligo-dev/core 1.0.0-beta.13 → 1.0.0-beta.15
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/db/schema/billing.d.ts +207 -2
- package/dist/db/schema/billing.d.ts.map +1 -1
- package/dist/db/schema/billing.js +32 -3
- package/dist/db/schema/billing.js.map +1 -1
- package/dist/db/schema/usage.d.ts +20 -2
- package/dist/db/schema/usage.d.ts.map +1 -1
- package/dist/db/schema/usage.js +17 -9
- package/dist/db/schema/usage.js.map +1 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +7 -1
- package/dist/env.js.map +1 -1
- package/dist/identity/index.d.ts +4 -3
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +3 -2
- package/dist/identity/index.js.map +1 -1
- package/dist/identity/service.d.ts +20 -4
- package/dist/identity/service.d.ts.map +1 -1
- package/dist/identity/service.js +78 -3
- package/dist/identity/service.js.map +1 -1
- package/dist/identity/types.d.ts +28 -1
- package/dist/identity/types.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/types.d.ts +8 -1
- package/dist/notifications/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/db/migrations/0003_local_payments.sql +18 -0
- package/src/db/migrations/0004_usage_subjects.sql +5 -0
- package/src/db/migrations/README.md +31 -2
- package/src/db/migrations/legacy-chain.json +13 -4
- package/src/db/migrations/meta/0003_snapshot.json +4501 -0
- package/src/db/migrations/meta/0004_snapshot.json +4504 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema/billing.ts +39 -3
- package/src/db/schema/usage.ts +17 -9
- package/src/env.ts +10 -3
- package/src/identity/index.ts +4 -1
- package/src/identity/service.ts +106 -5
- package/src/identity/types.ts +30 -0
- package/src/notifications/index.ts +5 -1
- package/src/notifications/types.ts +9 -1
|
@@ -22,6 +22,20 @@
|
|
|
22
22
|
"when": 1789808201382,
|
|
23
23
|
"tag": "0002_webhook_claim_lease",
|
|
24
24
|
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "7",
|
|
29
|
+
"when": 1790179158533,
|
|
30
|
+
"tag": "0003_local_payments",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "7",
|
|
36
|
+
"when": 1790259860351,
|
|
37
|
+
"tag": "0004_usage_subjects",
|
|
38
|
+
"breakpoints": true
|
|
25
39
|
}
|
|
26
40
|
]
|
|
27
41
|
}
|
package/src/db/schema/billing.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plans, subscriptions, credit ledgers, purchases
|
|
3
|
-
* Amounts in the credit ledgers are micros of their
|
|
2
|
+
* Plans, subscriptions, credit ledgers, purchases, local payments and
|
|
3
|
+
* Stripe events. Amounts in the credit ledgers are micros of their
|
|
4
|
+
* currency; amounts a buyer paid are minor units of theirs.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import {
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
index,
|
|
14
15
|
unique,
|
|
15
16
|
} from "drizzle-orm/pg-core";
|
|
16
|
-
import { organization } from "./auth";
|
|
17
|
+
import { organization, users } from "./auth";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* The registered plan catalogue, copied into rows so a subscription can
|
|
@@ -135,6 +136,39 @@ export const creditPurchases = pgTable(
|
|
|
135
136
|
(table) => [index("credit_purchases_workspace_id_idx").on(table.workspaceId)]
|
|
136
137
|
);
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Invoices issued through a registered payment provider (QR-and-poll
|
|
141
|
+
* methods such as QPay, PIX or UPI), one row per invoice. The row is
|
|
142
|
+
* written when the invoice is opened, so settlement knows who pays and
|
|
143
|
+
* what was priced without trusting the browser; `fulfilled_at` is set
|
|
144
|
+
* in the same transaction that grants what was bought, so a payment is
|
|
145
|
+
* granted once however many polls see it paid.
|
|
146
|
+
*/
|
|
147
|
+
export const payments = pgTable(
|
|
148
|
+
"payments",
|
|
149
|
+
{
|
|
150
|
+
id: text("id").primaryKey(),
|
|
151
|
+
/** The PAYMENT_MODE the invoice was opened under; settlement asks the same provider. */
|
|
152
|
+
provider: text("provider").notNull(),
|
|
153
|
+
invoiceId: text("invoice_id").notNull().unique(),
|
|
154
|
+
workspaceId: text("workspace_id")
|
|
155
|
+
.notNull()
|
|
156
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
157
|
+
userId: text("user_id").references(() => users.id, {
|
|
158
|
+
onDelete: "set null",
|
|
159
|
+
}),
|
|
160
|
+
/** What was bought, in the product's words: a plan slug, a bundle id. */
|
|
161
|
+
reference: text("reference").notNull(),
|
|
162
|
+
/** The price, in minor units of `currency`. */
|
|
163
|
+
amountMinor: integer("amount_minor").notNull(),
|
|
164
|
+
currency: text("currency").notNull(),
|
|
165
|
+
status: text("status").notNull().default("pending"), // pending|paid|failed
|
|
166
|
+
fulfilledAt: timestamp("fulfilled_at"),
|
|
167
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
168
|
+
},
|
|
169
|
+
(table) => [index("payments_workspace_id_idx").on(table.workspaceId)]
|
|
170
|
+
);
|
|
171
|
+
|
|
138
172
|
/** Every Stripe event, for idempotent webhook processing and audit. */
|
|
139
173
|
export const financeEvents = pgTable("finance_events", {
|
|
140
174
|
id: text("id").primaryKey(),
|
|
@@ -201,6 +235,8 @@ export type CreditBalance = typeof creditBalances.$inferSelect;
|
|
|
201
235
|
export type InsertCreditBalance = typeof creditBalances.$inferInsert;
|
|
202
236
|
export type CreditPurchase = typeof creditPurchases.$inferSelect;
|
|
203
237
|
export type InsertCreditPurchase = typeof creditPurchases.$inferInsert;
|
|
238
|
+
export type Payment = typeof payments.$inferSelect;
|
|
239
|
+
export type InsertPayment = typeof payments.$inferInsert;
|
|
204
240
|
export type FinanceEvent = typeof financeEvents.$inferSelect;
|
|
205
241
|
export type InsertFinanceEvent = typeof financeEvents.$inferInsert;
|
|
206
242
|
export type BillingSettings = typeof billingSettings.$inferSelect;
|
package/src/db/schema/usage.ts
CHANGED
|
@@ -27,10 +27,12 @@ export const usageRecords = pgTable(
|
|
|
27
27
|
workspaceId: text("workspace_id")
|
|
28
28
|
.notNull()
|
|
29
29
|
.references(() => organization.id, { onDelete: "cascade" }),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
/** Null for work no signed-in user started: a job, an anonymous request. */
|
|
31
|
+
userId: text("user_id").references(() => users.id, {
|
|
32
|
+
onDelete: "cascade",
|
|
33
|
+
}),
|
|
34
|
+
/** "ai_tokens", "fixed_charge", or "credit" (a refund or goodwill credit, negative). */
|
|
35
|
+
type: text("type").notNull(),
|
|
34
36
|
model: text("model"), // "gpt-4o", "gpt-4o-mini"
|
|
35
37
|
agent: text("agent"), // the product's agent slug
|
|
36
38
|
inputTokens: integer("input_tokens").notNull().default(0),
|
|
@@ -155,7 +157,8 @@ export const trialCredits = pgTable(
|
|
|
155
157
|
);
|
|
156
158
|
|
|
157
159
|
/**
|
|
158
|
-
* One row per (
|
|
160
|
+
* One row per (subject, endpoint, bucket); a window other than a minute
|
|
161
|
+
* is part of the endpoint key (`export@86400s`). Each request upserts the
|
|
159
162
|
* row and atomically increments `count`, so N concurrent requests observe
|
|
160
163
|
* 1..N and exactly `limit` are admitted. Old buckets are deleted by the
|
|
161
164
|
* billing-maintenance job.
|
|
@@ -164,13 +167,18 @@ export const rateLimitEntries = pgTable(
|
|
|
164
167
|
"rate_limit_entries",
|
|
165
168
|
{
|
|
166
169
|
id: text("id").primaryKey(),
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Who is counted: a workspace id, or any key the caller chose (a
|
|
172
|
+
* hashed IP). Not a foreign key, so a subject need not be a
|
|
173
|
+
* workspace; rows expire with their window either way.
|
|
174
|
+
*/
|
|
175
|
+
workspaceId: text("workspace_id").notNull(),
|
|
170
176
|
endpoint: text("endpoint").notNull().default("chat"),
|
|
171
177
|
requestedAt: timestamp("requested_at").notNull().defaultNow(),
|
|
172
|
-
/**
|
|
178
|
+
/** The start of the window this row counts, floored to its length. */
|
|
173
179
|
minuteBucket: timestamp("minute_bucket").notNull(),
|
|
180
|
+
/** The window's length, for cleanup: a row expires when its window has closed. */
|
|
181
|
+
windowSeconds: integer("window_seconds").notNull().default(60),
|
|
174
182
|
/** Requests observed in this bucket — incremented via upsert */
|
|
175
183
|
count: integer("count").notNull().default(1),
|
|
176
184
|
},
|
package/src/env.ts
CHANGED
|
@@ -76,6 +76,7 @@ export function validateEnv(): {
|
|
|
76
76
|
} {
|
|
77
77
|
const errors: string[] = [];
|
|
78
78
|
const warnings: string[] = [];
|
|
79
|
+
const unsetOptional: string[] = [];
|
|
79
80
|
|
|
80
81
|
for (const envVar of ENV_VARS) {
|
|
81
82
|
let value = process.env[envVar.name];
|
|
@@ -97,9 +98,7 @@ export function validateEnv(): {
|
|
|
97
98
|
`Missing required env var: ${envVar.name} — ${envVar.description}`
|
|
98
99
|
);
|
|
99
100
|
} else {
|
|
100
|
-
|
|
101
|
-
`Missing optional env var: ${envVar.name} — ${envVar.description}`
|
|
102
|
-
);
|
|
101
|
+
unsetOptional.push(envVar.name);
|
|
103
102
|
}
|
|
104
103
|
continue;
|
|
105
104
|
}
|
|
@@ -113,6 +112,14 @@ export function validateEnv(): {
|
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
|
|
115
|
+
// One line for all of them: each is a feature left off, which is the
|
|
116
|
+
// point of its being optional, not a fault to repeat on every boot.
|
|
117
|
+
if (unsetOptional.length > 0) {
|
|
118
|
+
warnings.push(
|
|
119
|
+
`Optional env vars not set, their features stay off: ${unsetOptional.join(", ")}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
// Resend rejects a send without a sender on a verified domain, and the
|
|
117
124
|
// framework has no domain of its own to fall back on. Loops takes the
|
|
118
125
|
// sender from each template, so only Resend is checked.
|
package/src/identity/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Privacy-facing reads and mutations over the user identity graph: fact
|
|
3
|
-
* listing and deletion,
|
|
3
|
+
* listing and deletion, the profile snapshot's write, full-identity
|
|
4
|
+
* export, and the memory-audit trail.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
export {
|
|
7
8
|
listFacts,
|
|
8
9
|
deleteFact,
|
|
10
|
+
saveProfileSnapshot,
|
|
9
11
|
exportIdentity,
|
|
10
12
|
getAuditTrail,
|
|
11
13
|
} from "./service";
|
|
@@ -19,6 +21,7 @@ export {
|
|
|
19
21
|
export type {
|
|
20
22
|
IdentityActor,
|
|
21
23
|
IdentityExport,
|
|
24
|
+
SaveProfileSnapshotInput,
|
|
22
25
|
UserFact,
|
|
23
26
|
UserMemory,
|
|
24
27
|
UserProfileSnapshot,
|
package/src/identity/service.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-only reads and mutations over the user identity graph: fact
|
|
3
|
-
* listing and deletion, a full data export,
|
|
3
|
+
* listing and deletion, the profile snapshot's write, a full data export,
|
|
4
|
+
* and the memory-audit trail.
|
|
4
5
|
*
|
|
5
6
|
* `deleteFact` does not touch the cached profile snapshot; profile synthesis
|
|
6
|
-
* is the product's AI code, so a caller that owns it re-triggers it
|
|
7
|
+
* is the product's AI code, so a caller that owns it re-triggers it and
|
|
8
|
+
* stores the result with `saveProfileSnapshot`.
|
|
7
9
|
*
|
|
8
10
|
* Callers pass a resolved actor (workspaceId, userId); core cannot depend on
|
|
9
11
|
* `@intelligo-dev/auth`. Every query filters by both ids. Every mutation
|
|
10
12
|
* writes a `user_memory_audit` row. Failures throw `IdentityServiceError`.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
15
|
+
import { and, desc, eq, sql } from "drizzle-orm";
|
|
14
16
|
import { db } from "../db";
|
|
15
17
|
import {
|
|
16
18
|
userFacts,
|
|
@@ -18,10 +20,18 @@ import {
|
|
|
18
20
|
userProfileSnapshots,
|
|
19
21
|
userMemoryAudit,
|
|
20
22
|
} from "../db/schema";
|
|
21
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
UserFact,
|
|
25
|
+
UserMemoryAuditRow,
|
|
26
|
+
UserProfileSnapshot,
|
|
27
|
+
} from "../db/schema";
|
|
22
28
|
import { recordMemoryAudit } from "./audit";
|
|
23
29
|
import { IdentityServiceError } from "./errors";
|
|
24
|
-
import type {
|
|
30
|
+
import type {
|
|
31
|
+
IdentityActor,
|
|
32
|
+
IdentityExport,
|
|
33
|
+
SaveProfileSnapshotInput,
|
|
34
|
+
} from "./types";
|
|
25
35
|
|
|
26
36
|
/**
|
|
27
37
|
* Returns the fact if the actor owns it. Throws
|
|
@@ -200,3 +210,94 @@ export async function exportIdentity(
|
|
|
200
210
|
audit,
|
|
201
211
|
};
|
|
202
212
|
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Store the actor's synthesized profile — one row per user per workspace,
|
|
216
|
+
* inserted on the first synthesis and replaced on every later one. The
|
|
217
|
+
* version is bumped in the same statement (`ON CONFLICT … version + 1`),
|
|
218
|
+
* so two syntheses racing each other get two versions rather than one
|
|
219
|
+
* overwriting the other's number; it starts at 1.
|
|
220
|
+
*
|
|
221
|
+
* A field left `undefined` keeps what the row holds (null on insert); an
|
|
222
|
+
* explicit `null` clears it. `synthesizedAt` defaults to now. Records an
|
|
223
|
+
* audit row (`snapshot`, `create` or `update`) in the same transaction,
|
|
224
|
+
* attributed to `input.actorKind` — `system_job` unless the caller says
|
|
225
|
+
* otherwise. Throws `invalid_input` for an empty summary.
|
|
226
|
+
*/
|
|
227
|
+
export async function saveProfileSnapshot(
|
|
228
|
+
actor: IdentityActor,
|
|
229
|
+
input: SaveProfileSnapshotInput
|
|
230
|
+
): Promise<UserProfileSnapshot> {
|
|
231
|
+
if (!input.summary || input.summary.trim().length === 0) {
|
|
232
|
+
throw new IdentityServiceError("invalid_input", "summary is required");
|
|
233
|
+
}
|
|
234
|
+
if (input.factsDigest === undefined || input.factsDigest === null) {
|
|
235
|
+
throw new IdentityServiceError("invalid_input", "factsDigest is required");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const replaced = {
|
|
239
|
+
summary: input.summary,
|
|
240
|
+
factsDigest: input.factsDigest,
|
|
241
|
+
synthesizedAt: input.synthesizedAt ?? new Date(),
|
|
242
|
+
...definedOnly({
|
|
243
|
+
summaryEn: input.summaryEn,
|
|
244
|
+
summaryMn: input.summaryMn,
|
|
245
|
+
activeGoals: input.activeGoals,
|
|
246
|
+
personalitySignals: input.personalitySignals,
|
|
247
|
+
relationshipNotes: input.relationshipNotes,
|
|
248
|
+
synthesizedByModel: input.synthesizedByModel,
|
|
249
|
+
nextSynthesisAt: input.nextSynthesisAt,
|
|
250
|
+
triggerReason: input.triggerReason,
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return db.transaction(async (tx) => {
|
|
255
|
+
const [row] = await tx
|
|
256
|
+
.insert(userProfileSnapshots)
|
|
257
|
+
.values({
|
|
258
|
+
userId: actor.userId,
|
|
259
|
+
workspaceId: actor.workspaceId,
|
|
260
|
+
version: 1,
|
|
261
|
+
...replaced,
|
|
262
|
+
})
|
|
263
|
+
.onConflictDoUpdate({
|
|
264
|
+
target: [userProfileSnapshots.userId, userProfileSnapshots.workspaceId],
|
|
265
|
+
set: {
|
|
266
|
+
...replaced,
|
|
267
|
+
version: sql`${userProfileSnapshots.version} + 1`,
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
.returning();
|
|
271
|
+
|
|
272
|
+
if (!row) {
|
|
273
|
+
throw new IdentityServiceError(
|
|
274
|
+
"database_error",
|
|
275
|
+
"Failed to save profile snapshot"
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await recordMemoryAudit(
|
|
280
|
+
{
|
|
281
|
+
userId: actor.userId,
|
|
282
|
+
workspaceId: actor.workspaceId,
|
|
283
|
+
targetKind: "snapshot",
|
|
284
|
+
targetId: actor.userId,
|
|
285
|
+
action: row.version === 1 ? "create" : "update",
|
|
286
|
+
actorKind: input.actorKind ?? "system_job",
|
|
287
|
+
actorId: input.actorId,
|
|
288
|
+
afterValue: { version: row.version, triggerReason: row.triggerReason },
|
|
289
|
+
reason: input.reason,
|
|
290
|
+
},
|
|
291
|
+
tx
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
return row;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The entries whose value is not `undefined` — `null` is kept, to clear. */
|
|
299
|
+
function definedOnly<T extends Record<string, unknown>>(fields: T): Partial<T> {
|
|
300
|
+
return Object.fromEntries(
|
|
301
|
+
Object.entries(fields).filter(([, value]) => value !== undefined)
|
|
302
|
+
) as Partial<T>;
|
|
303
|
+
}
|
package/src/identity/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AuditActorKind,
|
|
3
|
+
SynthesisTriggerReason,
|
|
2
4
|
UserFact,
|
|
3
5
|
UserMemory,
|
|
4
6
|
UserProfileSnapshot,
|
|
@@ -26,3 +28,31 @@ export type IdentityExport = {
|
|
|
26
28
|
snapshot: UserProfileSnapshot | null;
|
|
27
29
|
audit: UserMemoryAuditRow[];
|
|
28
30
|
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A synthesized profile, as `saveProfileSnapshot` stores it. The actor
|
|
34
|
+
* supplies the tenancy; the version is the store's.
|
|
35
|
+
*/
|
|
36
|
+
export type SaveProfileSnapshotInput = {
|
|
37
|
+
/** The summary a prompt hydrates from, in the product's primary language. */
|
|
38
|
+
summary: string;
|
|
39
|
+
summaryEn?: string | null;
|
|
40
|
+
summaryMn?: string | null;
|
|
41
|
+
/** The structured digest the summary was written from. */
|
|
42
|
+
factsDigest: unknown;
|
|
43
|
+
activeGoals?: unknown;
|
|
44
|
+
personalitySignals?: unknown;
|
|
45
|
+
relationshipNotes?: unknown;
|
|
46
|
+
/** The model that wrote it, or a label for a deterministic synthesis. */
|
|
47
|
+
synthesizedByModel?: string | null;
|
|
48
|
+
/** Default: now. */
|
|
49
|
+
synthesizedAt?: Date;
|
|
50
|
+
/** When a scheduled re-synthesis may next pick this user up. */
|
|
51
|
+
nextSynthesisAt?: Date | null;
|
|
52
|
+
triggerReason?: SynthesisTriggerReason | null;
|
|
53
|
+
/** Who the audit row names. Default `"system_job"`. */
|
|
54
|
+
actorKind?: AuditActorKind;
|
|
55
|
+
actorId?: string;
|
|
56
|
+
/** Recorded on the audit row. */
|
|
57
|
+
reason?: string;
|
|
58
|
+
};
|
|
@@ -8,7 +8,11 @@ import { notifications } from "../db/schema";
|
|
|
8
8
|
import { eq, and, desc, count } from "drizzle-orm";
|
|
9
9
|
import type { CreateNotificationParams } from "./types";
|
|
10
10
|
|
|
11
|
-
export type {
|
|
11
|
+
export type {
|
|
12
|
+
BuiltInNotificationType,
|
|
13
|
+
NotificationType,
|
|
14
|
+
CreateNotificationParams,
|
|
15
|
+
} from "./types";
|
|
12
16
|
|
|
13
17
|
export {
|
|
14
18
|
triggerQuotaNotification,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** The notification types the framework itself creates. */
|
|
2
|
+
export type BuiltInNotificationType =
|
|
2
3
|
| "quota_warning_80"
|
|
3
4
|
| "quota_warning_100"
|
|
4
5
|
| "trial_warning_20"
|
|
@@ -8,6 +9,13 @@ export type NotificationType =
|
|
|
8
9
|
| "subscription_confirmed"
|
|
9
10
|
| "workspace_invitation";
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* A notification's `type`: one of the built-in types, or any string a
|
|
14
|
+
* product defines for its own notifications. `(string & {})` keeps the
|
|
15
|
+
* built-in names offered by autocompletion.
|
|
16
|
+
*/
|
|
17
|
+
export type NotificationType = BuiltInNotificationType | (string & {});
|
|
18
|
+
|
|
11
19
|
export interface CreateNotificationParams {
|
|
12
20
|
userId: string;
|
|
13
21
|
workspaceId?: string;
|