@intelligo-dev/core 1.0.0-beta.1 → 1.0.0-beta.13
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/NOTICE +6 -0
- package/README.md +102 -0
- package/dist/attachments/errors.d.ts +7 -0
- package/dist/attachments/errors.d.ts.map +1 -0
- package/dist/attachments/errors.js +11 -0
- package/dist/attachments/errors.js.map +1 -0
- package/dist/attachments/index.d.ts +8 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +7 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/service.d.ts +45 -0
- package/dist/attachments/service.d.ts.map +1 -0
- package/dist/attachments/service.js +100 -0
- package/dist/attachments/service.js.map +1 -0
- package/dist/attachments/types.d.ts +6 -0
- package/dist/attachments/types.d.ts.map +1 -0
- package/dist/attachments/types.js +2 -0
- package/dist/attachments/types.js.map +1 -0
- package/dist/conversations/errors.d.ts +2 -8
- package/dist/conversations/errors.d.ts.map +1 -1
- package/dist/conversations/errors.js +2 -8
- package/dist/conversations/errors.js.map +1 -1
- package/dist/conversations/index.d.ts +4 -12
- package/dist/conversations/index.d.ts.map +1 -1
- package/dist/conversations/index.js +2 -11
- package/dist/conversations/index.js.map +1 -1
- package/dist/conversations/service.d.ts +49 -61
- package/dist/conversations/service.d.ts.map +1 -1
- package/dist/conversations/service.js +110 -83
- package/dist/conversations/service.js.map +1 -1
- package/dist/conversations/types.d.ts +1 -4
- package/dist/conversations/types.d.ts.map +1 -1
- package/dist/conversations/types.js +0 -3
- package/dist/conversations/types.js.map +1 -1
- package/dist/db/client.d.ts +21 -10
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +43 -18
- package/dist/db/client.js.map +1 -1
- package/dist/db/index.d.ts +3 -3
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +3 -7
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema/agents.d.ts +14 -51
- package/dist/db/schema/agents.d.ts.map +1 -1
- package/dist/db/schema/agents.js +9 -21
- package/dist/db/schema/agents.js.map +1 -1
- package/dist/db/schema/ai.d.ts +86 -1556
- package/dist/db/schema/ai.d.ts.map +1 -1
- package/dist/db/schema/ai.js +32 -236
- package/dist/db/schema/ai.js.map +1 -1
- package/dist/db/schema/auth.d.ts +5 -34
- package/dist/db/schema/auth.d.ts.map +1 -1
- package/dist/db/schema/auth.js +25 -56
- package/dist/db/schema/auth.js.map +1 -1
- package/dist/db/schema/billing.d.ts +136 -107
- package/dist/db/schema/billing.d.ts.map +1 -1
- package/dist/db/schema/billing.js +72 -60
- package/dist/db/schema/billing.js.map +1 -1
- package/dist/db/schema/feature-flags.d.ts +3 -12
- package/dist/db/schema/feature-flags.d.ts.map +1 -1
- package/dist/db/schema/feature-flags.js +3 -12
- package/dist/db/schema/feature-flags.js.map +1 -1
- package/dist/db/schema/identity.d.ts +12 -274
- package/dist/db/schema/identity.d.ts.map +1 -1
- package/dist/db/schema/identity.js +19 -104
- package/dist/db/schema/identity.js.map +1 -1
- package/dist/db/schema/index.d.ts +8 -15
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/index.js +12 -42
- package/dist/db/schema/index.js.map +1 -1
- package/dist/db/schema/notifications.d.ts +3 -17
- package/dist/db/schema/notifications.d.ts.map +1 -1
- package/dist/db/schema/notifications.js +3 -17
- package/dist/db/schema/notifications.js.map +1 -1
- package/dist/db/schema/usage.d.ts +94 -77
- package/dist/db/schema/usage.d.ts.map +1 -1
- package/dist/db/schema/usage.js +59 -89
- package/dist/db/schema/usage.js.map +1 -1
- package/dist/db/validate.d.ts +2 -11
- package/dist/db/validate.d.ts.map +1 -1
- package/dist/db/validate.js +6 -17
- package/dist/db/validate.js.map +1 -1
- package/dist/db/workspace-queries.d.ts +5 -29
- package/dist/db/workspace-queries.d.ts.map +1 -1
- package/dist/db/workspace-queries.js +5 -29
- package/dist/db/workspace-queries.js.map +1 -1
- package/dist/documents/classifier.d.ts +5 -12
- package/dist/documents/classifier.d.ts.map +1 -1
- package/dist/documents/classifier.js +16 -16
- package/dist/documents/classifier.js.map +1 -1
- package/dist/documents/errors.d.ts +2 -10
- package/dist/documents/errors.d.ts.map +1 -1
- package/dist/documents/errors.js +2 -10
- package/dist/documents/errors.js.map +1 -1
- package/dist/documents/index.d.ts +4 -13
- package/dist/documents/index.d.ts.map +1 -1
- package/dist/documents/index.js +3 -12
- package/dist/documents/index.js.map +1 -1
- package/dist/documents/service.d.ts +19 -26
- package/dist/documents/service.d.ts.map +1 -1
- package/dist/documents/service.js +44 -38
- package/dist/documents/service.js.map +1 -1
- package/dist/documents/types.d.ts +6 -10
- package/dist/documents/types.d.ts.map +1 -1
- package/dist/documents/types.js +0 -3
- package/dist/documents/types.js.map +1 -1
- package/dist/email/brand.d.ts +17 -0
- package/dist/email/brand.d.ts.map +1 -0
- package/dist/email/brand.js +9 -0
- package/dist/email/brand.js.map +1 -0
- package/dist/email/index.d.ts +4 -4
- package/dist/email/index.js +4 -4
- package/dist/email/provider.d.ts +1 -7
- package/dist/email/provider.d.ts.map +1 -1
- package/dist/email/provider.js +10 -19
- package/dist/email/provider.js.map +1 -1
- package/dist/email/send.d.ts +12 -12
- package/dist/email/send.d.ts.map +1 -1
- package/dist/email/send.js +5 -19
- package/dist/email/send.js.map +1 -1
- package/dist/email/senders.d.ts +17 -30
- package/dist/email/senders.d.ts.map +1 -1
- package/dist/email/senders.js +33 -47
- package/dist/email/senders.js.map +1 -1
- package/dist/email/templates/base-layout.d.ts +1 -1
- package/dist/email/templates/base-layout.d.ts.map +1 -1
- package/dist/email/templates/base-layout.js +4 -6
- package/dist/email/templates/base-layout.js.map +1 -1
- package/dist/email/templates/index.d.ts +10 -10
- package/dist/email/templates/index.d.ts.map +1 -1
- package/dist/email/templates/index.js +10 -16
- package/dist/email/templates/index.js.map +1 -1
- package/dist/email/templates/password-reset.d.ts +2 -2
- package/dist/email/templates/password-reset.d.ts.map +1 -1
- package/dist/email/templates/password-reset.js +10 -22
- package/dist/email/templates/password-reset.js.map +1 -1
- package/dist/email/templates/payment-failed.d.ts +2 -2
- package/dist/email/templates/payment-failed.d.ts.map +1 -1
- package/dist/email/templates/payment-failed.js +11 -24
- package/dist/email/templates/payment-failed.js.map +1 -1
- package/dist/email/templates/quota-warning.d.ts +4 -4
- package/dist/email/templates/quota-warning.d.ts.map +1 -1
- package/dist/email/templates/quota-warning.js +19 -39
- package/dist/email/templates/quota-warning.js.map +1 -1
- package/dist/email/templates/subscription-confirmed.d.ts +2 -2
- package/dist/email/templates/subscription-confirmed.d.ts.map +1 -1
- package/dist/email/templates/subscription-confirmed.js +13 -28
- package/dist/email/templates/subscription-confirmed.js.map +1 -1
- package/dist/email/templates/trial-expiry.d.ts +2 -2
- package/dist/email/templates/trial-expiry.d.ts.map +1 -1
- package/dist/email/templates/trial-expiry.js +12 -36
- package/dist/email/templates/trial-expiry.js.map +1 -1
- package/dist/email/templates/trial-warning.d.ts +2 -2
- package/dist/email/templates/trial-warning.d.ts.map +1 -1
- package/dist/email/templates/trial-warning.js +15 -32
- package/dist/email/templates/trial-warning.js.map +1 -1
- package/dist/email/templates/verify-email.d.ts +2 -2
- package/dist/email/templates/verify-email.d.ts.map +1 -1
- package/dist/email/templates/verify-email.js +10 -22
- package/dist/email/templates/verify-email.js.map +1 -1
- package/dist/email/templates/welcome.d.ts +2 -2
- package/dist/email/templates/welcome.d.ts.map +1 -1
- package/dist/email/templates/welcome.js +13 -20
- package/dist/email/templates/welcome.js.map +1 -1
- package/dist/email/templates/workspace-invitation.d.ts +2 -2
- package/dist/email/templates/workspace-invitation.d.ts.map +1 -1
- package/dist/email/templates/workspace-invitation.js +12 -26
- package/dist/email/templates/workspace-invitation.js.map +1 -1
- package/dist/env.d.ts +5 -10
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +41 -31
- package/dist/env.js.map +1 -1
- package/dist/identity/audit.d.ts +11 -24
- package/dist/identity/audit.d.ts.map +1 -1
- package/dist/identity/audit.js +13 -27
- package/dist/identity/audit.js.map +1 -1
- package/dist/identity/errors.d.ts +4 -12
- package/dist/identity/errors.d.ts.map +1 -1
- package/dist/identity/errors.js +2 -8
- package/dist/identity/errors.js.map +1 -1
- package/dist/identity/index.d.ts +5 -13
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +4 -12
- package/dist/identity/index.js.map +1 -1
- package/dist/identity/service.d.ts +13 -47
- package/dist/identity/service.d.ts.map +1 -1
- package/dist/identity/service.js +31 -78
- package/dist/identity/service.js.map +1 -1
- package/dist/identity/types.d.ts +1 -4
- package/dist/identity/types.d.ts.map +1 -1
- package/dist/identity/types.js +0 -3
- package/dist/identity/types.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -10
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +7 -18
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +45 -49
- package/dist/logger.js.map +1 -1
- package/dist/money.d.ts +95 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +208 -0
- package/dist/money.js.map +1 -0
- package/dist/notifications/index.d.ts +9 -43
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +9 -49
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/triggers.d.ts +9 -32
- package/dist/notifications/triggers.d.ts.map +1 -1
- package/dist/notifications/triggers.js +21 -58
- package/dist/notifications/triggers.js.map +1 -1
- package/dist/notifications/types.d.ts +1 -10
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/notifications/types.js +0 -9
- package/dist/notifications/types.js.map +1 -1
- package/dist/prompt.d.ts +38 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +118 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +32 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +60 -0
- package/dist/registry.js.map +1 -0
- package/dist/request-context.d.ts +49 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +100 -0
- package/dist/request-context.js.map +1 -0
- package/dist/storage/index.d.ts +65 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +109 -0
- package/dist/storage/index.js.map +1 -0
- package/package.json +74 -24
- package/src/attachments/errors.ts +18 -0
- package/src/attachments/index.ts +22 -0
- package/src/attachments/service.ts +178 -0
- package/src/attachments/types.ts +6 -0
- package/src/conversations/errors.ts +45 -0
- package/src/conversations/index.ts +39 -0
- package/src/conversations/service.ts +594 -0
- package/src/conversations/types.ts +15 -0
- package/src/db/client.ts +91 -0
- package/src/db/index.ts +3 -0
- package/src/db/migrations/0000_baseline.sql +652 -0
- package/src/db/migrations/0001_reconcile_chain_built_databases.sql +53 -0
- package/src/db/migrations/0002_webhook_claim_lease.sql +1 -0
- package/src/db/migrations/README.md +74 -36
- package/src/db/migrations/legacy-chain.json +197 -0
- package/src/db/migrations/meta/0000_snapshot.json +4182 -157
- package/src/db/migrations/meta/0002_snapshot.json +2478 -368
- package/src/db/migrations/meta/_journal.json +6 -279
- package/src/db/schema/agents.ts +57 -0
- package/src/db/schema/ai.ts +174 -0
- package/src/db/schema/auth.ts +183 -0
- package/src/db/schema/billing.ts +209 -0
- package/src/db/schema/feature-flags.ts +20 -0
- package/src/db/schema/identity.ts +250 -0
- package/src/db/schema/index.ts +12 -0
- package/src/db/schema/notifications.ts +31 -0
- package/src/db/schema/usage.ts +269 -0
- package/src/db/validate.ts +63 -0
- package/src/db/workspace-queries.ts +64 -0
- package/src/documents/classifier.ts +61 -0
- package/src/documents/errors.ts +44 -0
- package/src/documents/index.ts +24 -0
- package/src/documents/service.ts +256 -0
- package/src/documents/types.ts +24 -0
- package/src/email/brand.ts +24 -0
- package/src/email/index.ts +19 -0
- package/src/email/provider.ts +278 -0
- package/src/email/send.ts +169 -0
- package/src/email/senders.ts +292 -0
- package/src/email/templates/base-layout.tsx +145 -0
- package/src/email/templates/index.ts +28 -0
- package/src/email/templates/password-reset.tsx +72 -0
- package/src/email/templates/payment-failed.tsx +77 -0
- package/src/email/templates/quota-warning.tsx +118 -0
- package/src/email/templates/subscription-confirmed.tsx +91 -0
- package/src/email/templates/trial-expiry.tsx +124 -0
- package/src/email/templates/trial-warning.tsx +104 -0
- package/src/email/templates/verify-email.tsx +72 -0
- package/src/email/templates/welcome.tsx +64 -0
- package/src/email/templates/workspace-invitation.tsx +105 -0
- package/src/env.ts +180 -0
- package/src/identity/audit.ts +65 -0
- package/src/identity/errors.ts +44 -0
- package/src/identity/index.ts +26 -0
- package/src/identity/service.ts +202 -0
- package/src/identity/types.ts +28 -0
- package/src/index.ts +4 -0
- package/src/logger.ts +198 -0
- package/src/money.ts +270 -0
- package/src/notifications/index.ts +108 -0
- package/src/notifications/triggers.ts +158 -0
- package/src/notifications/types.ts +18 -0
- package/src/prompt.ts +142 -0
- package/src/registry.ts +75 -0
- package/src/request-context.ts +137 -0
- package/src/storage/index.ts +153 -0
- package/dist/db/schema/rag.d.ts +0 -171
- package/dist/db/schema/rag.d.ts.map +0 -1
- package/dist/db/schema/rag.js +0 -42
- package/dist/db/schema/rag.js.map +0 -1
- package/src/db/migrations/0000_sharp_night_nurse.sql +0 -52
- package/src/db/migrations/0001_hesitant_vapor.sql +0 -40
- package/src/db/migrations/0002_numerous_lady_vermin.sql +0 -235
- package/src/db/migrations/0003_mighty_jean_grey.sql +0 -37
- package/src/db/migrations/0004_cynical_grandmaster.sql +0 -2
- package/src/db/migrations/0005_careful_switch.sql +0 -8
- package/src/db/migrations/0006_tense_blue_marvel.sql +0 -16
- package/src/db/migrations/0007_next_squadron_sinister.sql +0 -1
- package/src/db/migrations/0008_powerful_scream.sql +0 -39
- package/src/db/migrations/0009_sad_ink.sql +0 -1
- package/src/db/migrations/0010_shocking_omega_flight.sql +0 -2
- package/src/db/migrations/0011_ambiguous_otto_octavius.sql +0 -151
- package/src/db/migrations/0011_hnsw_vector_index.sql +0 -17
- package/src/db/migrations/0012_user_profiles.sql +0 -30
- package/src/db/migrations/0013_trending_seasonal_columns.sql +0 -9
- package/src/db/migrations/0014_add_product_type_to_agents.sql +0 -6
- package/src/db/migrations/0015_new_v1_tables.sql +0 -149
- package/src/db/migrations/0016_document_types.sql +0 -30
- package/src/db/migrations/0017_document_metadata.sql +0 -2
- package/src/db/migrations/0018_identity_graph.sql +0 -200
- package/src/db/migrations/0019_db_driven_prompts.sql +0 -20
- package/src/db/migrations/0020_pending_extractions.sql +0 -35
- package/src/db/migrations/0021_rag_documents.sql +0 -39
- package/src/db/migrations/0022_user_quotas_usage_jsonb.sql +0 -70
- package/src/db/migrations/0023_user_facts_user_scoped_unique.sql +0 -17
- package/src/db/migrations/0024_user_memories_hnsw.sql +0 -23
- package/src/db/migrations/0025_audit_trigger_errcode.sql +0 -21
- package/src/db/migrations/0026_user_profile_snapshots_composite_pk.sql +0 -17
- package/src/db/migrations/0027_billing_settings_mnt_columns.sql +0 -54
- package/src/db/migrations/0028_trial_credits_normalized_email.sql +0 -37
- package/src/db/migrations/0029_usage_records_conversation_idx.sql +0 -13
- package/src/db/migrations/0030_pending_extractions_composite_idx.sql +0 -11
- package/src/db/migrations/0031_rag_documents_hnsw.sql +0 -16
- package/src/db/migrations/0032_rate_limit_unique_idx.sql +0 -16
- package/src/db/migrations/0033_pending_extractions_partial_idx.sql +0 -18
- package/src/db/migrations/0034_rate_limit_count.sql +0 -9
- package/src/db/migrations/0035_credit_reservations.sql +0 -24
- package/src/db/migrations/0036_executions_audit_jobs.sql +0 -96
- package/src/db/migrations/0037_rate_limit_minute_bucket.sql +0 -46
- package/src/db/migrations/0038_user_quotas_generic_usage.sql +0 -25
- package/src/db/migrations/0039_platform_admin_impersonation.sql +0 -27
- package/src/db/migrations/0040_ee_audit_chain.sql +0 -23
- package/src/db/migrations/meta/0001_snapshot.json +0 -631
- package/src/db/migrations/meta/0003_snapshot.json +0 -2488
- package/src/db/migrations/meta/0004_snapshot.json +0 -2500
- package/src/db/migrations/meta/0005_snapshot.json +0 -2548
- package/src/db/migrations/meta/0006_snapshot.json +0 -2650
- package/src/db/migrations/meta/0007_snapshot.json +0 -2657
- package/src/db/migrations/meta/0008_snapshot.json +0 -2719
- package/src/db/migrations/meta/0009_snapshot.json +0 -2726
- package/src/db/migrations/meta/0010_snapshot.json +0 -2739
- package/src/db/migrations/meta/0011_snapshot.json +0 -3886
package/src/logger.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured pino logger. Emails and ids are redacted in production and
|
|
3
|
+
* passed through in development; credential fields are always redacted.
|
|
4
|
+
*
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { createLogger } from "@intelligo-dev/core/logger";
|
|
7
|
+
* const log = createLogger("MyModule");
|
|
8
|
+
* log.info("User created", { email: "user@example.com" });
|
|
9
|
+
* // Production: {"level":30,"module":"MyModule","msg":"User created","email":"u***@example.com"}
|
|
10
|
+
* // Development: {"level":30,"module":"MyModule","msg":"User created","email":"user@example.com"}
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import pino from "pino";
|
|
15
|
+
|
|
16
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Redact email addresses in production.
|
|
20
|
+
* user@example.com -> u***@example.com
|
|
21
|
+
*/
|
|
22
|
+
function redactEmail(email: string): string {
|
|
23
|
+
if (!isProduction) return email;
|
|
24
|
+
const [local, domain] = email.split("@");
|
|
25
|
+
if (!local || !domain) return "***@***";
|
|
26
|
+
return `${local[0]}***@${domain}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Redact IDs in production (workspace IDs, UUIDs).
|
|
31
|
+
* ws_abc123def456 -> ws_a...f456
|
|
32
|
+
* 550e8400-e29b-41d4-a716-446655440000 -> 550e...0000
|
|
33
|
+
*/
|
|
34
|
+
function redactId(id: string): string {
|
|
35
|
+
if (!isProduction) return id;
|
|
36
|
+
if (id.length <= 8) return id;
|
|
37
|
+
return `${id.slice(0, 4)}...${id.slice(-4)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Field name fragments whose values are always replaced with [REDACTED], in
|
|
42
|
+
* every environment. Exported so an error reporter's scrubber can share the
|
|
43
|
+
* list.
|
|
44
|
+
*/
|
|
45
|
+
export const CREDENTIAL_KEY_FRAGMENTS = [
|
|
46
|
+
"password",
|
|
47
|
+
"passwd",
|
|
48
|
+
"secret",
|
|
49
|
+
"token",
|
|
50
|
+
"apikey",
|
|
51
|
+
"api_key",
|
|
52
|
+
"authorization",
|
|
53
|
+
"auth_header",
|
|
54
|
+
"cookie",
|
|
55
|
+
"session_token",
|
|
56
|
+
"session_secret",
|
|
57
|
+
"credit_card",
|
|
58
|
+
"creditcard",
|
|
59
|
+
"card_number",
|
|
60
|
+
"cvv",
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
/** True when `key` contains any credential fragment (case-insensitive substring). */
|
|
64
|
+
export function isCredentialKey(key: string): boolean {
|
|
65
|
+
const lower = key.toLowerCase();
|
|
66
|
+
return CREDENTIAL_KEY_FRAGMENTS.some((needle) => lower.includes(needle));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* An identifier's key: `id`, `userId`, `workspace_id`. Matched as a
|
|
71
|
+
* suffix, not a substring, so `provider` or `paid` keep their values.
|
|
72
|
+
*/
|
|
73
|
+
function isIdKey(key: string): boolean {
|
|
74
|
+
return /^id$|[a-z0-9]Id$|[_-]id$/.test(key) || /workspace/i.test(key);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Nested objects and arrays are walked, to this depth, so a credential inside one is still found. */
|
|
78
|
+
const MAX_REDACT_DEPTH = 6;
|
|
79
|
+
|
|
80
|
+
function redactValue(key: string, value: unknown, depth: number): unknown {
|
|
81
|
+
if (isCredentialKey(key)) return "[REDACTED]";
|
|
82
|
+
|
|
83
|
+
if (value !== null && typeof value === "object") {
|
|
84
|
+
if (depth >= MAX_REDACT_DEPTH) return "[TRUNCATED]";
|
|
85
|
+
if (Array.isArray(value)) {
|
|
86
|
+
return value.map((item) => redactValue(key, item, depth + 1));
|
|
87
|
+
}
|
|
88
|
+
if (Object.getPrototypeOf(value) === Object.prototype) {
|
|
89
|
+
return redactObject(value as Record<string, unknown>, depth + 1);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Email and id redaction is production-only, so development logs show
|
|
95
|
+
// real values.
|
|
96
|
+
if (!isProduction || typeof value !== "string") return value;
|
|
97
|
+
if (key.toLowerCase().includes("email")) return redactEmail(value);
|
|
98
|
+
if (isIdKey(key)) return redactId(value);
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function redactObject(
|
|
103
|
+
data: Record<string, unknown>,
|
|
104
|
+
depth: number
|
|
105
|
+
): Record<string, unknown> {
|
|
106
|
+
const redacted: Record<string, unknown> = {};
|
|
107
|
+
for (const [key, value] of Object.entries(data)) {
|
|
108
|
+
redacted[key] = redactValue(key, value, depth);
|
|
109
|
+
}
|
|
110
|
+
return redacted;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function redactData(data: Record<string, unknown>): Record<string, unknown> {
|
|
114
|
+
return redactObject(data, 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const baseLogger = pino({
|
|
118
|
+
level: isProduction ? "info" : "debug",
|
|
119
|
+
formatters: {
|
|
120
|
+
level(label) {
|
|
121
|
+
return { level: label };
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
timestamp: isProduction ? pino.stdTimeFunctions.isoTime : false,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
class Logger {
|
|
128
|
+
private pino: pino.Logger;
|
|
129
|
+
|
|
130
|
+
constructor(context: string) {
|
|
131
|
+
this.pino = baseLogger.child({ module: context });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
info(message: string, data?: Record<string, unknown>) {
|
|
135
|
+
if (data) {
|
|
136
|
+
this.pino.info(redactData(data), message);
|
|
137
|
+
} else {
|
|
138
|
+
this.pino.info(message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
warn(message: string, data?: Record<string, unknown>) {
|
|
143
|
+
if (data) {
|
|
144
|
+
this.pino.warn(redactData(data), message);
|
|
145
|
+
} else {
|
|
146
|
+
this.pino.warn(message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
error(message: string, data?: Record<string, unknown>) {
|
|
151
|
+
if (data) {
|
|
152
|
+
this.pino.error(redactData(data), message);
|
|
153
|
+
} else {
|
|
154
|
+
this.pino.error(message);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
debug(message: string, data?: Record<string, unknown>) {
|
|
159
|
+
if (data) {
|
|
160
|
+
this.pino.debug(redactData(data), message);
|
|
161
|
+
} else {
|
|
162
|
+
this.pino.debug(message);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createLogger(context: string): Logger {
|
|
168
|
+
return new Logger(context);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A logger writing to a custom pino destination, for capturing output in
|
|
173
|
+
* tests.
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
export function _createLoggerWithStream(
|
|
177
|
+
context: string,
|
|
178
|
+
stream: pino.DestinationStream
|
|
179
|
+
): Logger {
|
|
180
|
+
const customBase = pino(
|
|
181
|
+
{
|
|
182
|
+
level: isProduction ? "info" : "debug",
|
|
183
|
+
formatters: {
|
|
184
|
+
level(label) {
|
|
185
|
+
return { level: label };
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
timestamp: false,
|
|
189
|
+
},
|
|
190
|
+
stream
|
|
191
|
+
);
|
|
192
|
+
const instance = new Logger(context);
|
|
193
|
+
(instance as any).pino = customBase.child({ module: context });
|
|
194
|
+
return instance;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { Logger };
|
|
198
|
+
export const logger = createLogger("app");
|
package/src/money.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money, in one unit, with the currency attached. Imports nothing:
|
|
3
|
+
* `@intelligo-dev/executions/pricing` is a zero-import leaf that client
|
|
4
|
+
* bundles reach, and it needs these types.
|
|
5
|
+
*
|
|
6
|
+
* **Micros, not minor units.** A chat turn on a cheap model costs a
|
|
7
|
+
* fraction of a cent; rounding it to whole cents overcharges by more than a
|
|
8
|
+
* third on every request.
|
|
9
|
+
*
|
|
10
|
+
* **Currency on the amount, not in a global.** A bare `number` lets one
|
|
11
|
+
* unit be sold as another; here the currency travels with the amount and
|
|
12
|
+
* `add` refuses to mix two.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
declare const isoTag: unique symbol;
|
|
16
|
+
declare const microsTag: unique symbol;
|
|
17
|
+
|
|
18
|
+
/** An ISO-4217 alphabetic code, upper-cased and validated. */
|
|
19
|
+
export type CurrencyCode = string & { readonly [isoTag]: true };
|
|
20
|
+
|
|
21
|
+
/** An integer number of millionths of one major currency unit. */
|
|
22
|
+
export type Micros = number & { readonly [microsTag]: true };
|
|
23
|
+
|
|
24
|
+
/** An amount and the currency it is denominated in. */
|
|
25
|
+
export type Money = {
|
|
26
|
+
readonly amount: Micros;
|
|
27
|
+
readonly currency: CurrencyCode;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Thrown for every misuse in this module, so callers can catch one type. */
|
|
31
|
+
export class MoneyError extends Error {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
constructor(code: string, message: string) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.name = "MoneyError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MICROS_PER_UNIT = 1_000_000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Currencies whose minor unit is not the usual hundredth; everything else
|
|
44
|
+
* is 2. Getting this wrong charges ¥100,000 instead of ¥1,000, because
|
|
45
|
+
* Stripe takes zero-decimal currencies in whole units.
|
|
46
|
+
*/
|
|
47
|
+
const MINOR_EXPONENT: Readonly<Record<string, 0 | 2 | 3>> = {
|
|
48
|
+
BIF: 0,
|
|
49
|
+
CLP: 0,
|
|
50
|
+
DJF: 0,
|
|
51
|
+
GNF: 0,
|
|
52
|
+
ISK: 0,
|
|
53
|
+
JPY: 0,
|
|
54
|
+
KMF: 0,
|
|
55
|
+
KRW: 0,
|
|
56
|
+
MGA: 0,
|
|
57
|
+
MNT: 0,
|
|
58
|
+
PYG: 0,
|
|
59
|
+
RWF: 0,
|
|
60
|
+
UGX: 0,
|
|
61
|
+
UYI: 0,
|
|
62
|
+
VND: 0,
|
|
63
|
+
VUV: 0,
|
|
64
|
+
XAF: 0,
|
|
65
|
+
XOF: 0,
|
|
66
|
+
XPF: 0,
|
|
67
|
+
BHD: 3,
|
|
68
|
+
IQD: 3,
|
|
69
|
+
JOD: 3,
|
|
70
|
+
KWD: 3,
|
|
71
|
+
LYD: 3,
|
|
72
|
+
OMR: 3,
|
|
73
|
+
TND: 3,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validates and normalises an ISO-4217 code. Shape only: a list would go
|
|
78
|
+
* stale and block a deployment selling in a code it does not know.
|
|
79
|
+
* `minorExponent` gives an unknown code its default.
|
|
80
|
+
*/
|
|
81
|
+
export function currency(code: string): CurrencyCode {
|
|
82
|
+
const upper = code.toUpperCase();
|
|
83
|
+
if (!/^[A-Z]{3}$/.test(upper)) {
|
|
84
|
+
throw new MoneyError(
|
|
85
|
+
"invalid_currency",
|
|
86
|
+
`"${code}" is not an ISO-4217 alphabetic code (three letters).`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return upper as CurrencyCode;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* An integer count of micros, up to `Number.MAX_SAFE_INTEGER` (about 9
|
|
94
|
+
* billion major units) — low enough to catch a value in the wrong unit.
|
|
95
|
+
*/
|
|
96
|
+
export function micros(value: number): Micros {
|
|
97
|
+
if (!Number.isInteger(value)) {
|
|
98
|
+
throw new MoneyError(
|
|
99
|
+
"not_an_integer",
|
|
100
|
+
`Micros must be a whole number; received ${value}. Convert with fromMajor/fromMinor rather than multiplying by hand.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (!Number.isSafeInteger(value)) {
|
|
104
|
+
throw new MoneyError(
|
|
105
|
+
"out_of_range",
|
|
106
|
+
`${value} micros exceeds the safe integer range.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return value as Micros;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function money(amount: number, code: string | CurrencyCode): Money {
|
|
113
|
+
return { amount: micros(amount), currency: currency(code) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** How many decimal places this currency's minor unit has. */
|
|
117
|
+
export function minorExponent(code: CurrencyCode): 0 | 2 | 3 {
|
|
118
|
+
return MINOR_EXPONENT[code] ?? 2;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `12.34` USD → `12_340_000` micros. */
|
|
122
|
+
export function fromMajor(value: number, code: string | CurrencyCode): Money {
|
|
123
|
+
return {
|
|
124
|
+
amount: micros(Math.round(value * MICROS_PER_UNIT)),
|
|
125
|
+
currency: currency(code),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** `1234` cents → `12_340_000` micros. */
|
|
130
|
+
export function fromMinor(value: number, code: string | CurrencyCode): Money {
|
|
131
|
+
const c = currency(code);
|
|
132
|
+
const scale = MICROS_PER_UNIT / 10 ** minorExponent(c);
|
|
133
|
+
return { amount: micros(Math.round(value * scale)), currency: c };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The integer a payment provider wants: whole units for a zero-decimal
|
|
138
|
+
* currency, hundredths for most, thousandths for a few.
|
|
139
|
+
*
|
|
140
|
+
* Rounds up. A fraction of a minor unit is money the deployment has
|
|
141
|
+
* already spent, and rounding it away means eating the difference on
|
|
142
|
+
* every transaction.
|
|
143
|
+
*/
|
|
144
|
+
export function toMinor(value: Money): number {
|
|
145
|
+
const scale = MICROS_PER_UNIT / 10 ** minorExponent(value.currency);
|
|
146
|
+
return Math.ceil(value.amount / scale);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The amount as a plain decimal number, for display and for charts. */
|
|
150
|
+
export function toMajor(value: Money): number {
|
|
151
|
+
return value.amount / MICROS_PER_UNIT;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const zero = (code: string | CurrencyCode): Money => ({
|
|
155
|
+
amount: 0 as Micros,
|
|
156
|
+
currency: currency(code),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
function sameCurrency(a: Money, b: Money, op: string): void {
|
|
160
|
+
if (a.currency !== b.currency) {
|
|
161
|
+
throw new MoneyError(
|
|
162
|
+
"currency_mismatch",
|
|
163
|
+
`Cannot ${op} ${a.currency} and ${b.currency}. Convert one first — an exchange rate is a decision, not a coercion.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function add(a: Money, b: Money): Money {
|
|
169
|
+
sameCurrency(a, b, "add");
|
|
170
|
+
return { amount: micros(a.amount + b.amount), currency: a.currency };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function subtract(a: Money, b: Money): Money {
|
|
174
|
+
sameCurrency(a, b, "subtract");
|
|
175
|
+
return { amount: micros(a.amount - b.amount), currency: a.currency };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Scale by a plain factor — a margin multiplier, a percentage, a count. */
|
|
179
|
+
export function multiply(value: Money, factor: number): Money {
|
|
180
|
+
if (!Number.isFinite(factor)) {
|
|
181
|
+
throw new MoneyError("invalid_factor", `Factor must be finite.`);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
amount: micros(Math.ceil(value.amount * factor)),
|
|
185
|
+
currency: value.currency,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function compare(a: Money, b: Money): number {
|
|
190
|
+
sameCurrency(a, b, "compare");
|
|
191
|
+
return a.amount === b.amount ? 0 : a.amount < b.amount ? -1 : 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const isZero = (value: Money): boolean => value.amount === 0;
|
|
195
|
+
export const isNegative = (value: Money): boolean => value.amount < 0;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convert at an explicit rate, expressed in micros so the rate itself
|
|
199
|
+
* carries no float error: `3_450_000_000` is 3450.0.
|
|
200
|
+
*
|
|
201
|
+
* There is no ambient rate and no default. A framework that guesses an
|
|
202
|
+
* exchange rate is inventing money.
|
|
203
|
+
*/
|
|
204
|
+
export function convert(
|
|
205
|
+
value: Money,
|
|
206
|
+
to: string | CurrencyCode,
|
|
207
|
+
rateMicros: number
|
|
208
|
+
): Money {
|
|
209
|
+
const target = currency(to);
|
|
210
|
+
if (!Number.isInteger(rateMicros) || rateMicros <= 0) {
|
|
211
|
+
throw new MoneyError(
|
|
212
|
+
"invalid_rate",
|
|
213
|
+
`Rate must be a positive integer number of micros; received ${rateMicros}.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (target === value.currency) {
|
|
217
|
+
if (rateMicros !== MICROS_PER_UNIT) {
|
|
218
|
+
throw new MoneyError(
|
|
219
|
+
"invalid_rate",
|
|
220
|
+
`Converting ${value.currency} to itself at ${rateMicros / MICROS_PER_UNIT} is not a conversion.`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
amount: micros(Math.ceil((value.amount * rateMicros) / MICROS_PER_UNIT)),
|
|
227
|
+
currency: target,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Two significant digits is enough to read a sub-cent amount by. */
|
|
232
|
+
const SIGNIFICANT_DIGITS = 2;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* How many decimals an amount needs to be legible.
|
|
236
|
+
*
|
|
237
|
+
* The currency's own minor unit, except when that would render a real
|
|
238
|
+
* charge as nothing: one chat turn costs a fraction of a cent, and a
|
|
239
|
+
* usage page that says every request cost `$0.00` is worse than one
|
|
240
|
+
* that says `$0.0048`. Below the minor unit the amount is shown to two
|
|
241
|
+
* significant digits, capped at the six decimals micros can hold.
|
|
242
|
+
*/
|
|
243
|
+
export function displayFractionDigits(value: Money): number {
|
|
244
|
+
const digits = minorExponent(value.currency);
|
|
245
|
+
const major = Math.abs(toMajor(value));
|
|
246
|
+
// Zero, and anything that still rounds to a visible minor unit, reads
|
|
247
|
+
// the way the currency is normally written.
|
|
248
|
+
if (major === 0 || major >= 10 ** -digits / 2) return digits;
|
|
249
|
+
const leadingZeros = Math.floor(-Math.log10(major));
|
|
250
|
+
return Math.min(6, leadingZeros + SIGNIFICANT_DIGITS);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Formats for a human, in their locale. `Intl` owns the symbol, its
|
|
255
|
+
* position and the separators.
|
|
256
|
+
*/
|
|
257
|
+
export function formatMoney(
|
|
258
|
+
value: Money,
|
|
259
|
+
locale: string,
|
|
260
|
+
options: Intl.NumberFormatOptions = {}
|
|
261
|
+
): string {
|
|
262
|
+
const digits = displayFractionDigits(value);
|
|
263
|
+
return new Intl.NumberFormat(locale, {
|
|
264
|
+
style: "currency",
|
|
265
|
+
currency: value.currency,
|
|
266
|
+
minimumFractionDigits: digits,
|
|
267
|
+
maximumFractionDigits: digits,
|
|
268
|
+
...options,
|
|
269
|
+
}).format(toMajor(value));
|
|
270
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User notifications. User-scoped, not workspace-scoped, so a user sees
|
|
3
|
+
* their notifications from every workspace.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db } from "../db";
|
|
7
|
+
import { notifications } from "../db/schema";
|
|
8
|
+
import { eq, and, desc, count } from "drizzle-orm";
|
|
9
|
+
import type { CreateNotificationParams } from "./types";
|
|
10
|
+
|
|
11
|
+
export type { NotificationType, CreateNotificationParams } from "./types";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
triggerQuotaNotification,
|
|
15
|
+
triggerTrialNotification,
|
|
16
|
+
triggerPaymentFailedNotification,
|
|
17
|
+
triggerTeamMemberJoinedNotification,
|
|
18
|
+
} from "./triggers";
|
|
19
|
+
|
|
20
|
+
/** Creates a notification; `metadata` is stored as a JSON string. */
|
|
21
|
+
export async function createNotification(params: CreateNotificationParams) {
|
|
22
|
+
const [notification] = await db
|
|
23
|
+
.insert(notifications)
|
|
24
|
+
.values({
|
|
25
|
+
userId: params.userId,
|
|
26
|
+
workspaceId: params.workspaceId,
|
|
27
|
+
type: params.type,
|
|
28
|
+
title: params.title,
|
|
29
|
+
message: params.message,
|
|
30
|
+
metadata: params.metadata ? JSON.stringify(params.metadata) : null,
|
|
31
|
+
})
|
|
32
|
+
.returning();
|
|
33
|
+
|
|
34
|
+
return notification;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The user's unread notifications, newest first. */
|
|
38
|
+
export async function getUnreadNotifications(
|
|
39
|
+
userId: string,
|
|
40
|
+
limit: number = 20
|
|
41
|
+
) {
|
|
42
|
+
const rows = await db
|
|
43
|
+
.select()
|
|
44
|
+
.from(notifications)
|
|
45
|
+
.where(
|
|
46
|
+
and(eq(notifications.userId, userId), eq(notifications.isRead, false))
|
|
47
|
+
)
|
|
48
|
+
.orderBy(desc(notifications.createdAt))
|
|
49
|
+
.limit(limit);
|
|
50
|
+
|
|
51
|
+
return rows.map(parseMetadata);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** All of the user's notifications, newest first. */
|
|
55
|
+
export async function getNotifications(userId: string, limit: number = 50) {
|
|
56
|
+
const rows = await db
|
|
57
|
+
.select()
|
|
58
|
+
.from(notifications)
|
|
59
|
+
.where(eq(notifications.userId, userId))
|
|
60
|
+
.orderBy(desc(notifications.createdAt))
|
|
61
|
+
.limit(limit);
|
|
62
|
+
|
|
63
|
+
return rows.map(parseMetadata);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getNotificationCount(userId: string): Promise<number> {
|
|
67
|
+
const [result] = await db
|
|
68
|
+
.select({ count: count() })
|
|
69
|
+
.from(notifications)
|
|
70
|
+
.where(
|
|
71
|
+
and(eq(notifications.userId, userId), eq(notifications.isRead, false))
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return result?.count ?? 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Filters on userId as well, so users can only mark their own. */
|
|
78
|
+
export async function markAsRead(notificationId: string, userId: string) {
|
|
79
|
+
await db
|
|
80
|
+
.update(notifications)
|
|
81
|
+
.set({ isRead: true })
|
|
82
|
+
.where(
|
|
83
|
+
and(
|
|
84
|
+
eq(notifications.id, notificationId),
|
|
85
|
+
eq(notifications.userId, userId)
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function markAllAsRead(userId: string) {
|
|
91
|
+
await db
|
|
92
|
+
.update(notifications)
|
|
93
|
+
.set({ isRead: true })
|
|
94
|
+
.where(
|
|
95
|
+
and(eq(notifications.userId, userId), eq(notifications.isRead, false))
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseMetadata<T extends { metadata: string | null }>(
|
|
100
|
+
notification: T
|
|
101
|
+
): T & { metadata: Record<string, unknown> | null } {
|
|
102
|
+
return {
|
|
103
|
+
...notification,
|
|
104
|
+
metadata: notification.metadata
|
|
105
|
+
? (JSON.parse(notification.metadata) as Record<string, unknown>)
|
|
106
|
+
: null,
|
|
107
|
+
};
|
|
108
|
+
}
|