@friggframework/core 2.0.0--canary.622.16ee19d.0 → 2.0.0--canary.622.faffe5f.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.
Files changed (48) hide show
  1. package/application/commands/usage-commands.js +5 -4
  2. package/application/index.js +9 -18
  3. package/core/create-handler.js +17 -6
  4. package/generated/prisma-mongodb/edge.js +3 -3
  5. package/generated/prisma-mongodb/index.d.ts +80 -55
  6. package/generated/prisma-mongodb/index.js +3 -3
  7. package/generated/prisma-mongodb/package.json +1 -1
  8. package/generated/prisma-mongodb/schema.prisma +5 -3
  9. package/generated/prisma-mongodb/wasm.js +3 -3
  10. package/generated/prisma-postgresql/edge.js +3 -3
  11. package/generated/prisma-postgresql/index.d.ts +92 -16
  12. package/generated/prisma-postgresql/index.js +3 -3
  13. package/generated/prisma-postgresql/package.json +1 -1
  14. package/generated/prisma-postgresql/schema.prisma +5 -3
  15. package/generated/prisma-postgresql/wasm.js +3 -3
  16. package/handlers/integration-event-dispatcher.js +1 -1
  17. package/index.js +1 -1
  18. package/integrations/integration-base.js +23 -4
  19. package/modules/requester/requester.js +12 -2
  20. package/package.json +5 -5
  21. package/prisma-mongodb/schema.prisma +5 -3
  22. package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +3 -3
  23. package/prisma-postgresql/schema.prisma +5 -3
  24. package/reporting/reporting-router.js +1 -1
  25. package/reporting/use-cases/list-integrations-report.js +2 -2
  26. package/telemetry/README.md +22 -6
  27. package/telemetry/bind-telemetry-context.js +1 -1
  28. package/telemetry/canonical-counters.js +1 -1
  29. package/telemetry/exporters/exporter-factory.js +2 -2
  30. package/telemetry/instrument-handler.js +1 -1
  31. package/telemetry/no-op-telemetry.js +1 -1
  32. package/telemetry/north-star.js +1 -1
  33. package/telemetry/otel-telemetry.js +1 -1
  34. package/telemetry/plugin-subscribers-singleton.js +2 -2
  35. package/telemetry/plugin-subscribers.js +1 -1
  36. package/telemetry/telemetry-config.js +1 -1
  37. package/telemetry/telemetry-context.js +1 -1
  38. package/telemetry/telemetry-event-bus.js +2 -2
  39. package/telemetry/telemetry-service.js +3 -3
  40. package/telemetry/telemetry-singleton.js +1 -1
  41. package/telemetry/usage-rollup-singleton.js +1 -1
  42. package/telemetry/usage-rollup-subscriber.js +3 -3
  43. package/telemetry/usage-windows.js +1 -1
  44. package/usage/README.md +6 -4
  45. package/usage/repositories/usage-repository-documentdb.js +193 -9
  46. package/usage/repositories/usage-repository-interface.js +1 -1
  47. package/usage/repositories/usage-repository-postgres.js +22 -12
  48. package/usage/tracked-metrics.js +1 -1
@@ -380,13 +380,13 @@ const config = {
380
380
  }
381
381
  }
382
382
  },
383
- "inlineSchema": "// Frigg Framework - Prisma Schema (PostgreSQL)\n// PostgreSQL database schema for enterprise integration platform\n// Converted from MongoDB schema for relational database support\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma-postgresql\"\n binaryTargets = [\"native\", \"rhel-openssl-3.0.x\"] // native for local dev, rhel for Lambda deployment\n // Library engine (default since Prisma 3.x): Rust query engine loads as a\n // Node-API addon inside the same process. The binary engine forks a child\n // query-engine subprocess and communicates over a local HTTP/IPC pipe with\n // NO client-side timeout — a zombied child wedges the Node process until\n // Lambda's 900s cap. Switching to library eliminates that entire class of\n // silent hangs. See friggframework/frigg#580 for the investigation.\n engineType = \"library\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\n// ============================================================================\n// USER MODELS\n// ============================================================================\n\n/// User model with discriminator pattern support\n/// Replaces Mongoose discriminators (IndividualUser, OrganizationUser)\nmodel User {\n id Int @id @default(autoincrement())\n type UserType\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // IndividualUser fields (nullable for organizations)\n email String?\n username String?\n hashword String? // Bcrypt hashed password (handled in application layer)\n appUserId String?\n organizationId Int?\n\n // Self-referential relation for organization membership\n organization User? @relation(\"OrgMembers\", fields: [organizationId], references: [id], onDelete: NoAction, onUpdate: NoAction)\n members User[] @relation(\"OrgMembers\")\n\n // OrganizationUser fields (nullable for individuals)\n appOrgId String?\n name String?\n\n // Relations\n tokens Token[]\n credentials Credential[]\n entities Entity[]\n integrations Integration[]\n processes Process[]\n\n @@unique([username, appUserId])\n @@index([type])\n @@index([appUserId])\n @@index([organizationId])\n}\n\nenum UserType {\n INDIVIDUAL\n ORGANIZATION\n}\n\n// ============================================================================\n// AUTHENTICATION MODELS\n// ============================================================================\n\n/// Authentication tokens with expiration\n/// Bcrypt hashed tokens stored (handled in application layer)\nmodel Token {\n id Int @id @default(autoincrement())\n token String // Bcrypt hashed\n created DateTime @default(now())\n expires DateTime?\n userId Int\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([expires])\n}\n\n// ============================================================================\n// CREDENTIAL & ENTITY MODELS\n// ============================================================================\n\n/// OAuth credentials and API tokens\n/// All sensitive data encrypted with KMS at rest\nmodel Credential {\n id Int @id @default(autoincrement())\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n authIsValid Boolean?\n externalId String?\n\n // Dynamic OAuth fields stored as JSON (encrypted via Prisma middleware)\n // Contains: access_token, refresh_token, domain, expires_in, token_type, etc.\n data Json @default(\"{}\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n entities Entity[]\n\n @@index([userId])\n @@index([externalId])\n}\n\n/// External service entities (API connections)\nmodel Entity {\n id Int @id @default(autoincrement())\n credentialId Int?\n credential Credential? @relation(fields: [credentialId], references: [id], onDelete: SetNull)\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n name String?\n moduleName String?\n externalId String?\n\n data Json @default(\"{}\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations - many-to-many with implicit join tables\n integrations Integration[]\n syncs Sync[]\n\n dataIdentifiers DataIdentifier[]\n associationObjects AssociationObject[]\n\n @@index([userId])\n @@index([externalId])\n @@index([moduleName])\n @@index([credentialId])\n}\n\n// ============================================================================\n// INTEGRATION MODELS\n// ============================================================================\n\n/// Main integration configuration and state\nmodel Integration {\n id Int @id @default(autoincrement())\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n status IntegrationStatus @default(IN_CREATION)\n\n // Configuration and version\n config Json? // Integration configuration object\n version String?\n\n // Entity references (many-to-many via implicit join table)\n entities Entity[]\n\n // Message arrays (stored as JSON)\n errors Json @default(\"[]\")\n warnings Json @default(\"[]\")\n info Json @default(\"[]\")\n logs Json @default(\"[]\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n associations Association[]\n syncs Sync[]\n mappings IntegrationMapping[]\n processes Process[]\n\n @@index([userId])\n @@index([status])\n}\n\nenum IntegrationStatus {\n ENABLED\n NEEDS_CONFIG\n PROCESSING\n DISABLED\n ERROR\n IN_CREATION\n IN_DELETION\n}\n\n/// Integration-specific data mappings\n/// All mapping data encrypted with KMS\nmodel IntegrationMapping {\n id Int @id @default(autoincrement())\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n sourceId String?\n\n // Encrypted mapping data (handled via Prisma middleware)\n mapping Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([integrationId, sourceId])\n @@index([integrationId])\n @@index([sourceId])\n}\n\n// ============================================================================\n// SYNC MODELS\n// ============================================================================\n\n/// Bidirectional data synchronization tracking\nmodel Sync {\n id Int @id @default(autoincrement())\n integrationId Int?\n integration Integration? @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n\n // Entity references (many-to-many via implicit join table)\n entities Entity[]\n\n hash String\n name String\n\n // Data identifiers (extracted to separate model)\n dataIdentifiers DataIdentifier[]\n\n @@index([integrationId])\n @@index([hash])\n @@index([name])\n}\n\n/// Data identifier for sync operations\n/// Replaces nested array structure in Mongoose\nmodel DataIdentifier {\n id Int @id @default(autoincrement())\n syncId Int?\n sync Sync? @relation(fields: [syncId], references: [id], onDelete: Cascade)\n entityId Int\n entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)\n\n // Identifier data (can be any structure)\n idData Json\n\n hash String\n\n @@index([syncId])\n @@index([entityId])\n @@index([hash])\n}\n\n// ============================================================================\n// ASSOCIATION MODELS\n// ============================================================================\n\n/// Entity associations with cardinality tracking\nmodel Association {\n id Int @id @default(autoincrement())\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n name String\n type AssociationType\n primaryObject String\n\n // Associated objects (extracted to separate model)\n objects AssociationObject[]\n\n @@index([integrationId])\n @@index([name])\n}\n\n/// Association object entry\n/// Replaces nested array structure in Mongoose\nmodel AssociationObject {\n id Int @id @default(autoincrement())\n associationId Int\n association Association @relation(fields: [associationId], references: [id], onDelete: Cascade)\n entityId Int\n entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)\n objectType String\n objId String\n metadata Json? // Optional metadata\n\n @@index([associationId])\n @@index([entityId])\n}\n\nenum AssociationType {\n ONE_TO_MANY\n ONE_TO_ONE\n MANY_TO_ONE\n}\n\n// ============================================================================\n// PROCESS MODELS\n// ============================================================================\n\n/// Generic Process Model - tracks any long-running operation\n/// Used for: CRM syncs, data migrations, bulk operations, etc.\nmodel Process {\n id Int @id @default(autoincrement())\n\n // Core references\n userId Int\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n\n // Process identification\n name String // e.g., \"zoho-crm-contact-sync\", \"pipedrive-lead-sync\"\n type String // e.g., \"CRM_SYNC\", \"DATA_MIGRATION\", \"BULK_OPERATION\"\n\n // State machine\n state String // Current state (integration-defined states)\n\n // Flexible storage\n context Json @default(\"{}\") // Process-specific data (pagination, metadata, etc.)\n results Json @default(\"{}\") // Process results and metrics\n\n // Hierarchy support - self-referential relation\n parentProcessId Int?\n parentProcess Process? @relation(\"ProcessHierarchy\", fields: [parentProcessId], references: [id], onDelete: SetNull)\n childProcesses Process[] @relation(\"ProcessHierarchy\")\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@index([integrationId])\n @@index([type])\n @@index([state])\n @@index([name])\n @@index([parentProcessId])\n}\n\n// ============================================================================\n// UTILITY MODELS\n// ============================================================================\n\n/// Generic state storage\nmodel State {\n id Int @id @default(autoincrement())\n state Json?\n}\n\n/// AWS API Gateway WebSocket connection tracking\nmodel WebsocketConnection {\n id Int @id @default(autoincrement())\n connectionId String?\n\n @@index([connectionId])\n}\n\n// ============================================================================\n// ADMIN PROCESS MODELS\n// ============================================================================\n\n/// Admin process state machine\nenum AdminScriptExecutionState {\n PENDING\n RUNNING\n COMPLETED\n FAILED\n}\n\n/// Admin process tracking (like Process but without user/integration FK)\n/// Used for: admin scripts, db migrations, system maintenance tasks\nmodel AdminScriptExecution {\n id Int @id @default(autoincrement())\n name String // e.g., \"oauth-token-refresh\", \"db-migration-xyz\"\n type String // e.g., \"ADMIN_SCRIPT\", \"DB_MIGRATION\"\n\n // State machine\n state AdminScriptExecutionState @default(PENDING)\n\n // Flexible storage (mirrors Process model pattern)\n context Json @default(\"{}\") // input, trigger, audit info, script version\n results Json @default(\"{}\") // output, logs, metrics, errors\n\n // Hierarchy support - self-referential relation\n parentExecutionId Int?\n parentExecution AdminScriptExecution? @relation(\"AdminScriptExecutionHierarchy\", fields: [parentExecutionId], references: [id], onDelete: SetNull)\n childExecutions AdminScriptExecution[] @relation(\"AdminScriptExecutionHierarchy\")\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([name, createdAt(sort: Desc)])\n @@index([state])\n @@index([type])\n}\n\n/// Script scheduling configuration for hybrid scheduling (SQS + EventBridge)\nmodel ScriptSchedule {\n id Int @id @default(autoincrement())\n scriptName String @unique\n enabled Boolean @default(false)\n cronExpression String?\n timezone String @default(\"UTC\")\n lastTriggeredAt DateTime?\n nextTriggerAt DateTime?\n\n // External Scheduler (e.g., AWS EventBridge Scheduler)\n externalScheduleId String?\n externalScheduleName String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([enabled])\n}\n\n/// ADR-011 durable usage counters. Deliberately isolated (ADR-010 Decision 3):\n/// NO userId and NO foreign key to Integration, so a user-scoped query can never\n/// return a usage row and usage history survives integration deletion. Not in\n/// the encryption registry — dimensions are bounded, non-sensitive.\nmodel UsageCounter {\n id Int @id @default(autoincrement())\n\n integrationId String\n integrationType String\n metric String\n window String // e.g. \"day:2026-07-05\" or \"hour:2026-07-05T14\"\n value Int @default(0)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([integrationId, integrationType, metric, window])\n @@index([metric])\n @@index([integrationType])\n}\n",
384
- "inlineSchemaHash": "9e7959de0f6626459773898a4f802f357d8949d5685d99a47bf79dacce894e07",
383
+ "inlineSchema": "// Frigg Framework - Prisma Schema (PostgreSQL)\n// PostgreSQL database schema for enterprise integration platform\n// Converted from MongoDB schema for relational database support\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma-postgresql\"\n binaryTargets = [\"native\", \"rhel-openssl-3.0.x\"] // native for local dev, rhel for Lambda deployment\n // Library engine (default since Prisma 3.x): Rust query engine loads as a\n // Node-API addon inside the same process. The binary engine forks a child\n // query-engine subprocess and communicates over a local HTTP/IPC pipe with\n // NO client-side timeout — a zombied child wedges the Node process until\n // Lambda's 900s cap. Switching to library eliminates that entire class of\n // silent hangs. See friggframework/frigg#580 for the investigation.\n engineType = \"library\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\n// ============================================================================\n// USER MODELS\n// ============================================================================\n\n/// User model with discriminator pattern support\n/// Replaces Mongoose discriminators (IndividualUser, OrganizationUser)\nmodel User {\n id Int @id @default(autoincrement())\n type UserType\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // IndividualUser fields (nullable for organizations)\n email String?\n username String?\n hashword String? // Bcrypt hashed password (handled in application layer)\n appUserId String?\n organizationId Int?\n\n // Self-referential relation for organization membership\n organization User? @relation(\"OrgMembers\", fields: [organizationId], references: [id], onDelete: NoAction, onUpdate: NoAction)\n members User[] @relation(\"OrgMembers\")\n\n // OrganizationUser fields (nullable for individuals)\n appOrgId String?\n name String?\n\n // Relations\n tokens Token[]\n credentials Credential[]\n entities Entity[]\n integrations Integration[]\n processes Process[]\n\n @@unique([username, appUserId])\n @@index([type])\n @@index([appUserId])\n @@index([organizationId])\n}\n\nenum UserType {\n INDIVIDUAL\n ORGANIZATION\n}\n\n// ============================================================================\n// AUTHENTICATION MODELS\n// ============================================================================\n\n/// Authentication tokens with expiration\n/// Bcrypt hashed tokens stored (handled in application layer)\nmodel Token {\n id Int @id @default(autoincrement())\n token String // Bcrypt hashed\n created DateTime @default(now())\n expires DateTime?\n userId Int\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([expires])\n}\n\n// ============================================================================\n// CREDENTIAL & ENTITY MODELS\n// ============================================================================\n\n/// OAuth credentials and API tokens\n/// All sensitive data encrypted with KMS at rest\nmodel Credential {\n id Int @id @default(autoincrement())\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n authIsValid Boolean?\n externalId String?\n\n // Dynamic OAuth fields stored as JSON (encrypted via Prisma middleware)\n // Contains: access_token, refresh_token, domain, expires_in, token_type, etc.\n data Json @default(\"{}\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n entities Entity[]\n\n @@index([userId])\n @@index([externalId])\n}\n\n/// External service entities (API connections)\nmodel Entity {\n id Int @id @default(autoincrement())\n credentialId Int?\n credential Credential? @relation(fields: [credentialId], references: [id], onDelete: SetNull)\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n name String?\n moduleName String?\n externalId String?\n\n data Json @default(\"{}\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations - many-to-many with implicit join tables\n integrations Integration[]\n syncs Sync[]\n\n dataIdentifiers DataIdentifier[]\n associationObjects AssociationObject[]\n\n @@index([userId])\n @@index([externalId])\n @@index([moduleName])\n @@index([credentialId])\n}\n\n// ============================================================================\n// INTEGRATION MODELS\n// ============================================================================\n\n/// Main integration configuration and state\nmodel Integration {\n id Int @id @default(autoincrement())\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n status IntegrationStatus @default(IN_CREATION)\n\n // Configuration and version\n config Json? // Integration configuration object\n version String?\n\n // Entity references (many-to-many via implicit join table)\n entities Entity[]\n\n // Message arrays (stored as JSON)\n errors Json @default(\"[]\")\n warnings Json @default(\"[]\")\n info Json @default(\"[]\")\n logs Json @default(\"[]\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n associations Association[]\n syncs Sync[]\n mappings IntegrationMapping[]\n processes Process[]\n\n @@index([userId])\n @@index([status])\n}\n\nenum IntegrationStatus {\n ENABLED\n NEEDS_CONFIG\n PROCESSING\n DISABLED\n ERROR\n IN_CREATION\n IN_DELETION\n}\n\n/// Integration-specific data mappings\n/// All mapping data encrypted with KMS\nmodel IntegrationMapping {\n id Int @id @default(autoincrement())\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n sourceId String?\n\n // Encrypted mapping data (handled via Prisma middleware)\n mapping Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([integrationId, sourceId])\n @@index([integrationId])\n @@index([sourceId])\n}\n\n// ============================================================================\n// SYNC MODELS\n// ============================================================================\n\n/// Bidirectional data synchronization tracking\nmodel Sync {\n id Int @id @default(autoincrement())\n integrationId Int?\n integration Integration? @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n\n // Entity references (many-to-many via implicit join table)\n entities Entity[]\n\n hash String\n name String\n\n // Data identifiers (extracted to separate model)\n dataIdentifiers DataIdentifier[]\n\n @@index([integrationId])\n @@index([hash])\n @@index([name])\n}\n\n/// Data identifier for sync operations\n/// Replaces nested array structure in Mongoose\nmodel DataIdentifier {\n id Int @id @default(autoincrement())\n syncId Int?\n sync Sync? @relation(fields: [syncId], references: [id], onDelete: Cascade)\n entityId Int\n entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)\n\n // Identifier data (can be any structure)\n idData Json\n\n hash String\n\n @@index([syncId])\n @@index([entityId])\n @@index([hash])\n}\n\n// ============================================================================\n// ASSOCIATION MODELS\n// ============================================================================\n\n/// Entity associations with cardinality tracking\nmodel Association {\n id Int @id @default(autoincrement())\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n name String\n type AssociationType\n primaryObject String\n\n // Associated objects (extracted to separate model)\n objects AssociationObject[]\n\n @@index([integrationId])\n @@index([name])\n}\n\n/// Association object entry\n/// Replaces nested array structure in Mongoose\nmodel AssociationObject {\n id Int @id @default(autoincrement())\n associationId Int\n association Association @relation(fields: [associationId], references: [id], onDelete: Cascade)\n entityId Int\n entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)\n objectType String\n objId String\n metadata Json? // Optional metadata\n\n @@index([associationId])\n @@index([entityId])\n}\n\nenum AssociationType {\n ONE_TO_MANY\n ONE_TO_ONE\n MANY_TO_ONE\n}\n\n// ============================================================================\n// PROCESS MODELS\n// ============================================================================\n\n/// Generic Process Model - tracks any long-running operation\n/// Used for: CRM syncs, data migrations, bulk operations, etc.\nmodel Process {\n id Int @id @default(autoincrement())\n\n // Core references\n userId Int\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n\n // Process identification\n name String // e.g., \"zoho-crm-contact-sync\", \"pipedrive-lead-sync\"\n type String // e.g., \"CRM_SYNC\", \"DATA_MIGRATION\", \"BULK_OPERATION\"\n\n // State machine\n state String // Current state (integration-defined states)\n\n // Flexible storage\n context Json @default(\"{}\") // Process-specific data (pagination, metadata, etc.)\n results Json @default(\"{}\") // Process results and metrics\n\n // Hierarchy support - self-referential relation\n parentProcessId Int?\n parentProcess Process? @relation(\"ProcessHierarchy\", fields: [parentProcessId], references: [id], onDelete: SetNull)\n childProcesses Process[] @relation(\"ProcessHierarchy\")\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@index([integrationId])\n @@index([type])\n @@index([state])\n @@index([name])\n @@index([parentProcessId])\n}\n\n// ============================================================================\n// UTILITY MODELS\n// ============================================================================\n\n/// Generic state storage\nmodel State {\n id Int @id @default(autoincrement())\n state Json?\n}\n\n/// AWS API Gateway WebSocket connection tracking\nmodel WebsocketConnection {\n id Int @id @default(autoincrement())\n connectionId String?\n\n @@index([connectionId])\n}\n\n// ============================================================================\n// ADMIN PROCESS MODELS\n// ============================================================================\n\n/// Admin process state machine\nenum AdminScriptExecutionState {\n PENDING\n RUNNING\n COMPLETED\n FAILED\n}\n\n/// Admin process tracking (like Process but without user/integration FK)\n/// Used for: admin scripts, db migrations, system maintenance tasks\nmodel AdminScriptExecution {\n id Int @id @default(autoincrement())\n name String // e.g., \"oauth-token-refresh\", \"db-migration-xyz\"\n type String // e.g., \"ADMIN_SCRIPT\", \"DB_MIGRATION\"\n\n // State machine\n state AdminScriptExecutionState @default(PENDING)\n\n // Flexible storage (mirrors Process model pattern)\n context Json @default(\"{}\") // input, trigger, audit info, script version\n results Json @default(\"{}\") // output, logs, metrics, errors\n\n // Hierarchy support - self-referential relation\n parentExecutionId Int?\n parentExecution AdminScriptExecution? @relation(\"AdminScriptExecutionHierarchy\", fields: [parentExecutionId], references: [id], onDelete: SetNull)\n childExecutions AdminScriptExecution[] @relation(\"AdminScriptExecutionHierarchy\")\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([name, createdAt(sort: Desc)])\n @@index([state])\n @@index([type])\n}\n\n/// Script scheduling configuration for hybrid scheduling (SQS + EventBridge)\nmodel ScriptSchedule {\n id Int @id @default(autoincrement())\n scriptName String @unique\n enabled Boolean @default(false)\n cronExpression String?\n timezone String @default(\"UTC\")\n lastTriggeredAt DateTime?\n nextTriggerAt DateTime?\n\n // External Scheduler (e.g., AWS EventBridge Scheduler)\n externalScheduleId String?\n externalScheduleName String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([enabled])\n}\n\n/// ADR-011 durable usage counters. Deliberately isolated (ADR-010 Decision 3):\n/// NO userId and NO foreign key to Integration, so a user-scoped query can never\n/// return a usage row and usage history survives integration deletion. Not in\n/// the encryption registry — dimensions are bounded, non-sensitive.\nmodel UsageCounter {\n id Int @id @default(autoincrement())\n\n integrationId String\n integrationType String\n metric String\n window String // e.g. \"day:2026-07-05\" or \"hour:2026-07-05T14\"\n value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([integrationId, integrationType, metric, window])\n // Covers totals() (metric + window prefix/range, no integrationType filter).\n @@index([metric, window])\n // Covers series() (metric + integrationType + window range).\n @@index([metric, integrationType, window])\n}\n",
384
+ "inlineSchemaHash": "631b0201a53d34f70785c0767b3b8b8aaa2a3d6087e0861dbbf93016ace630f0",
385
385
  "copyEngine": true
386
386
  }
387
387
  config.dirname = '/'
388
388
 
389
- config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"UserType\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hashword\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appUserId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrgMembers\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrgMembers\"},{\"name\":\"appOrgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokens\",\"kind\":\"object\",\"type\":\"Token\",\"relationName\":\"TokenToUser\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToUser\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToUser\"},{\"name\":\"integrations\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToUser\"},{\"name\":\"processes\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessToUser\"}],\"dbName\":null},\"Token\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"created\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"TokenToUser\"}],\"dbName\":null},\"Credential\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"CredentialToUser\"},{\"name\":\"authIsValid\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"externalId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"CredentialToEntity\"}],\"dbName\":null},\"Entity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"credential\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToEntity\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"EntityToUser\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"externalId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"integrations\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"EntityToIntegration\"},{\"name\":\"syncs\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"EntityToSync\"},{\"name\":\"dataIdentifiers\",\"kind\":\"object\",\"type\":\"DataIdentifier\",\"relationName\":\"DataIdentifierToEntity\"},{\"name\":\"associationObjects\",\"kind\":\"object\",\"type\":\"AssociationObject\",\"relationName\":\"AssociationObjectToEntity\"}],\"dbName\":null},\"Integration\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"IntegrationToUser\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"IntegrationStatus\"},{\"name\":\"config\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"version\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToIntegration\"},{\"name\":\"errors\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"warnings\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"info\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"logs\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"associations\",\"kind\":\"object\",\"type\":\"Association\",\"relationName\":\"AssociationToIntegration\"},{\"name\":\"syncs\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"IntegrationToSync\"},{\"name\":\"mappings\",\"kind\":\"object\",\"type\":\"IntegrationMapping\",\"relationName\":\"IntegrationToIntegrationMapping\"},{\"name\":\"processes\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"IntegrationToProcess\"}],\"dbName\":null},\"IntegrationMapping\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToIntegrationMapping\"},{\"name\":\"sourceId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"mapping\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Sync\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToSync\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToSync\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"dataIdentifiers\",\"kind\":\"object\",\"type\":\"DataIdentifier\",\"relationName\":\"DataIdentifierToSync\"}],\"dbName\":null},\"DataIdentifier\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"syncId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"sync\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"DataIdentifierToSync\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"entity\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"DataIdentifierToEntity\"},{\"name\":\"idData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Association\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"AssociationToIntegration\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"AssociationType\"},{\"name\":\"primaryObject\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"objects\",\"kind\":\"object\",\"type\":\"AssociationObject\",\"relationName\":\"AssociationToAssociationObject\"}],\"dbName\":null},\"AssociationObject\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"associationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"association\",\"kind\":\"object\",\"type\":\"Association\",\"relationName\":\"AssociationToAssociationObject\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"entity\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"AssociationObjectToEntity\"},{\"name\":\"objectType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"objId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metadata\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"Process\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ProcessToUser\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToProcess\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"context\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"results\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"parentProcessId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"parentProcess\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessHierarchy\"},{\"name\":\"childProcesses\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessHierarchy\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"State\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"WebsocketConnection\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"connectionId\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"AdminScriptExecution\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"enum\",\"type\":\"AdminScriptExecutionState\"},{\"name\":\"context\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"results\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"parentExecutionId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"parentExecution\",\"kind\":\"object\",\"type\":\"AdminScriptExecution\",\"relationName\":\"AdminScriptExecutionHierarchy\"},{\"name\":\"childExecutions\",\"kind\":\"object\",\"type\":\"AdminScriptExecution\",\"relationName\":\"AdminScriptExecutionHierarchy\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ScriptSchedule\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"scriptName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"enabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"cronExpression\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"timezone\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastTriggeredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"nextTriggerAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"externalScheduleId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"externalScheduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"UsageCounter\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"integrationType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metric\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"window\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
389
+ config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"UserType\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hashword\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appUserId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrgMembers\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrgMembers\"},{\"name\":\"appOrgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokens\",\"kind\":\"object\",\"type\":\"Token\",\"relationName\":\"TokenToUser\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToUser\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToUser\"},{\"name\":\"integrations\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToUser\"},{\"name\":\"processes\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessToUser\"}],\"dbName\":null},\"Token\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"created\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"TokenToUser\"}],\"dbName\":null},\"Credential\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"CredentialToUser\"},{\"name\":\"authIsValid\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"externalId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"CredentialToEntity\"}],\"dbName\":null},\"Entity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"credential\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToEntity\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"EntityToUser\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"externalId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"integrations\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"EntityToIntegration\"},{\"name\":\"syncs\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"EntityToSync\"},{\"name\":\"dataIdentifiers\",\"kind\":\"object\",\"type\":\"DataIdentifier\",\"relationName\":\"DataIdentifierToEntity\"},{\"name\":\"associationObjects\",\"kind\":\"object\",\"type\":\"AssociationObject\",\"relationName\":\"AssociationObjectToEntity\"}],\"dbName\":null},\"Integration\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"IntegrationToUser\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"IntegrationStatus\"},{\"name\":\"config\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"version\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToIntegration\"},{\"name\":\"errors\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"warnings\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"info\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"logs\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"associations\",\"kind\":\"object\",\"type\":\"Association\",\"relationName\":\"AssociationToIntegration\"},{\"name\":\"syncs\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"IntegrationToSync\"},{\"name\":\"mappings\",\"kind\":\"object\",\"type\":\"IntegrationMapping\",\"relationName\":\"IntegrationToIntegrationMapping\"},{\"name\":\"processes\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"IntegrationToProcess\"}],\"dbName\":null},\"IntegrationMapping\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToIntegrationMapping\"},{\"name\":\"sourceId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"mapping\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Sync\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToSync\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToSync\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"dataIdentifiers\",\"kind\":\"object\",\"type\":\"DataIdentifier\",\"relationName\":\"DataIdentifierToSync\"}],\"dbName\":null},\"DataIdentifier\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"syncId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"sync\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"DataIdentifierToSync\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"entity\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"DataIdentifierToEntity\"},{\"name\":\"idData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Association\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"AssociationToIntegration\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"AssociationType\"},{\"name\":\"primaryObject\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"objects\",\"kind\":\"object\",\"type\":\"AssociationObject\",\"relationName\":\"AssociationToAssociationObject\"}],\"dbName\":null},\"AssociationObject\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"associationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"association\",\"kind\":\"object\",\"type\":\"Association\",\"relationName\":\"AssociationToAssociationObject\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"entity\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"AssociationObjectToEntity\"},{\"name\":\"objectType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"objId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metadata\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"Process\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ProcessToUser\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToProcess\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"context\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"results\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"parentProcessId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"parentProcess\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessHierarchy\"},{\"name\":\"childProcesses\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessHierarchy\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"State\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"WebsocketConnection\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"connectionId\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"AdminScriptExecution\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"enum\",\"type\":\"AdminScriptExecutionState\"},{\"name\":\"context\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"results\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"parentExecutionId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"parentExecution\",\"kind\":\"object\",\"type\":\"AdminScriptExecution\",\"relationName\":\"AdminScriptExecutionHierarchy\"},{\"name\":\"childExecutions\",\"kind\":\"object\",\"type\":\"AdminScriptExecution\",\"relationName\":\"AdminScriptExecutionHierarchy\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ScriptSchedule\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"scriptName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"enabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"cronExpression\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"timezone\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastTriggeredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"nextTriggerAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"externalScheduleId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"externalScheduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"UsageCounter\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"integrationType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metric\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"window\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"BigInt\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
390
390
  defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
391
391
  config.engineWasm = {
392
392
  getRuntime: async () => require('./query_engine_bg.js'),
@@ -25,7 +25,7 @@ class IntegrationEventDispatcher {
25
25
  }
26
26
 
27
27
  /**
28
- * Resolve + invoke a handler, auto-instrumented (ADR-011 Decision 2). This
28
+ * Resolve + invoke a handler, auto-instrumented. This
29
29
  * is the seam for queue/webhook/defined-route dispatch; the `this.on` path
30
30
  * (user actions, lifecycle) is instrumented in IntegrationBase.send().
31
31
  */
package/index.js CHANGED
@@ -143,7 +143,7 @@ module.exports = {
143
143
  createReportingRouter,
144
144
  createReportingRepository,
145
145
 
146
- // telemetry (ADR-011)
146
+ // telemetry
147
147
  createTelemetry,
148
148
  getTelemetry,
149
149
  createUsageRepository,
@@ -79,7 +79,7 @@ class IntegrationBase {
79
79
  // Tier 3 Integration Extensions — see packages/core/integrations/EXTENSIONS.md
80
80
  // Shape: { [bindingName]: { extension, handlers?: { [eventName]: methodName } } }
81
81
  extensions: {},
82
- // Usage-counter opt-in (ADR-011). Declaring a canonical key opts into the
82
+ // Usage-counter opt-in. Declaring a canonical key opts into the
83
83
  // cross-integration comparison report + durable rollup; custom keys are
84
84
  // comparable within this integration type. Shape:
85
85
  // usage: { canonical: ['records.synced', ...], custom: { 'deals.enriched': { unit, label } } }
@@ -108,7 +108,7 @@ class IntegrationBase {
108
108
  this.messages = { errors: [], warnings: [] };
109
109
  this._isHydrated = false;
110
110
 
111
- // Telemetry (ADR-011): every instance carries the service so integration
111
+ // Telemetry: every instance carries the service so integration
112
112
  // code can call `this.telemetry.*`. Extracted before the record check so
113
113
  // passing only `telemetry` never triggers a hollow hydration. Bound to
114
114
  // this instance so emissions auto-carry `integration_type` for the usage
@@ -222,6 +222,25 @@ class IntegrationBase {
222
222
  };
223
223
 
224
224
  this._isHydrated = Boolean(this.id);
225
+
226
+ // Log the instance-open exactly once per hydrated
227
+ // instance, carrying the standard identifier set — so an integration is
228
+ // visible in telemetry even on a path that never dispatches a handler.
229
+ // High-cardinality ids ride the bus context (3rd arg), never metric
230
+ // labels; only the bounded integration_type is an attribute.
231
+ if (this._isHydrated && !this._instantiationLogged) {
232
+ this._instantiationLogged = true;
233
+ try {
234
+ this.telemetry.event(
235
+ 'frigg.integration.instantiated',
236
+ { integration_type: this.constructor?.Definition?.name },
237
+ this.getTelemetryContext()
238
+ );
239
+ } catch (_) {
240
+ // Telemetry must never break integration hydration.
241
+ }
242
+ }
243
+
225
244
  return this;
226
245
  }
227
246
 
@@ -230,7 +249,7 @@ class IntegrationBase {
230
249
  }
231
250
 
232
251
  /**
233
- * Standard telemetry identifier set (ADR-011 Decision 3). Assembled from the
252
+ * Standard telemetry identifier set. Assembled from the
234
253
  * hydrated record (integrationId, userId, version), the static Definition
235
254
  * (integrationType, version fallback), and the environment (stage, appName).
236
255
  * High-cardinality ids (integrationId, userId) ride span baggage only — never
@@ -793,7 +812,7 @@ class IntegrationBase {
793
812
  `Event ${event} is not defined in the Integration event object`
794
813
  );
795
814
  }
796
- // Auto-instrument (ADR-011 Decision 2). This is the seam for user
815
+ // Auto-instrument. This is the seam for user
797
816
  // actions, config-options, and lifecycle events dispatched via `this.on`
798
817
  // (the queue/webhook/route paths go through IntegrationEventDispatcher).
799
818
  return instrumentHandler(
@@ -44,8 +44,18 @@ class Requester extends Delegate {
44
44
  // Instance methods can use this.fetch without differentiating
45
45
  this.fetch = get(params, 'fetch', fetch);
46
46
 
47
- // Telemetry (ADR-011). Defaults to the process singleton; overridable
47
+ // Telemetry. Defaults to the process singleton; overridable
48
48
  // for tests. Outbound requests are instrumented in `_request`.
49
+ //
50
+ // Attribution boundary: the `frigg.apimodule.requests` OTel metric fires
51
+ // for EVERY request (observability is complete). The durable per-
52
+ // integration `api.requests` usage counter, however, only rolls up
53
+ // requests carrying an integration context — set by the handler seams via
54
+ // AsyncLocalStorage (dispatched USER_ACTION/CRON/QUEUE/WEBHOOK paths). A
55
+ // request made before an integration exists (OAuth/token exchange, entity
56
+ // discovery during connection setup) has no integration to attribute to,
57
+ // so it is observable but not rolled up. Pass a context-bound `telemetry`
58
+ // (an integration's `this.telemetry`) to attribute out-of-band requests.
49
59
  this.telemetry = (params && params.telemetry) || getTelemetry();
50
60
  }
51
61
 
@@ -93,7 +103,7 @@ class Requester extends Delegate {
93
103
  };
94
104
 
95
105
  /**
96
- * Instrumenting entry point (ADR-011 P7). Wraps the whole logical request —
106
+ * Instrumenting entry point. Wraps the whole logical request —
97
107
  * including retry/refresh recursion — in a single span + one
98
108
  * `frigg.apimodule.requests` counter, emitted on the `i === 0` boundary so
99
109
  * retries are never double-counted. The full URL rides the span only; the
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.622.16ee19d.0",
4
+ "version": "2.0.0--canary.622.faffe5f.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -45,9 +45,9 @@
45
45
  }
46
46
  },
47
47
  "devDependencies": {
48
- "@friggframework/eslint-config": "2.0.0--canary.622.16ee19d.0",
49
- "@friggframework/prettier-config": "2.0.0--canary.622.16ee19d.0",
50
- "@friggframework/test": "2.0.0--canary.622.16ee19d.0",
48
+ "@friggframework/eslint-config": "2.0.0--canary.622.faffe5f.0",
49
+ "@friggframework/prettier-config": "2.0.0--canary.622.faffe5f.0",
50
+ "@friggframework/test": "2.0.0--canary.622.faffe5f.0",
51
51
  "@prisma/client": "^6.19.3",
52
52
  "@types/lodash": "4.17.15",
53
53
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -87,5 +87,5 @@
87
87
  "publishConfig": {
88
88
  "access": "public"
89
89
  },
90
- "gitHead": "16ee19d11c975c256a996503bafc70c062440ff0"
90
+ "gitHead": "faffe5ff8b3fea6bbb46a52502cbd5b5ecb35559"
91
91
  }
@@ -442,13 +442,15 @@ model UsageCounter {
442
442
  integrationType String
443
443
  metric String
444
444
  window String // e.g. "day:2026-07-05" or "hour:2026-07-05T14"
445
- value Int @default(0)
445
+ value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit
446
446
 
447
447
  createdAt DateTime @default(now())
448
448
  updatedAt DateTime @updatedAt
449
449
 
450
450
  @@unique([integrationId, integrationType, metric, window])
451
- @@index([metric])
452
- @@index([integrationType])
451
+ // Covers totals() (metric + window prefix/range, no integrationType filter).
452
+ @@index([metric, window])
453
+ // Covers series() (metric + integrationType + window range).
454
+ @@index([metric, integrationType, window])
453
455
  @@map("UsageCounter")
454
456
  }
@@ -9,7 +9,7 @@ CREATE TABLE "UsageCounter" (
9
9
  "integrationType" TEXT NOT NULL,
10
10
  "metric" TEXT NOT NULL,
11
11
  "window" TEXT NOT NULL,
12
- "value" INTEGER NOT NULL DEFAULT 0,
12
+ "value" BIGINT NOT NULL DEFAULT 0,
13
13
  "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
14
  "updatedAt" TIMESTAMP(3) NOT NULL,
15
15
 
@@ -20,7 +20,7 @@ CREATE TABLE "UsageCounter" (
20
20
  CREATE UNIQUE INDEX "UsageCounter_integrationId_integrationType_metric_window_key" ON "UsageCounter"("integrationId", "integrationType", "metric", "window");
21
21
 
22
22
  -- CreateIndex
23
- CREATE INDEX "UsageCounter_metric_idx" ON "UsageCounter"("metric");
23
+ CREATE INDEX "UsageCounter_metric_window_idx" ON "UsageCounter"("metric", "window");
24
24
 
25
25
  -- CreateIndex
26
- CREATE INDEX "UsageCounter_integrationType_idx" ON "UsageCounter"("integrationType");
26
+ CREATE INDEX "UsageCounter_metric_integrationType_window_idx" ON "UsageCounter"("metric", "integrationType", "window");
@@ -424,12 +424,14 @@ model UsageCounter {
424
424
  integrationType String
425
425
  metric String
426
426
  window String // e.g. "day:2026-07-05" or "hour:2026-07-05T14"
427
- value Int @default(0)
427
+ value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit
428
428
 
429
429
  createdAt DateTime @default(now())
430
430
  updatedAt DateTime @updatedAt
431
431
 
432
432
  @@unique([integrationId, integrationType, metric, window])
433
- @@index([metric])
434
- @@index([integrationType])
433
+ // Covers totals() (metric + window prefix/range, no integrationType filter).
434
+ @@index([metric, window])
435
+ // Covers series() (metric + integrationType + window range).
436
+ @@index([metric, integrationType, window])
435
437
  }
@@ -40,7 +40,7 @@ function buildTypeLabels() {
40
40
 
41
41
  function createReportingRouter() {
42
42
  const reportingRepository = createReportingRepository();
43
- // ADR-011 hand-off: enrich the report with feature-usage columns read from
43
+ // Enrich the report with feature-usage columns read from
44
44
  // the Frigg-owned usage store. Best-effort — a failure to construct it never
45
45
  // blocks the structural report.
46
46
  let usageRepository = null;
@@ -26,7 +26,7 @@ class ListIntegrationsReport {
26
26
  throw new Error('reportingRepository is required');
27
27
  }
28
28
  this.reportingRepository = reportingRepository;
29
- // Optional ADR-011 usage store (the ADR-010 hand-off). When present, the
29
+ // Optional usage store (the ADR-010 hand-off). When present, the
30
30
  // report's byType buckets are enriched with feature-usage columns read
31
31
  // ONLY from this store — never an external APM.
32
32
  this.usageRepository = usageRepository || null;
@@ -87,7 +87,7 @@ class ListIntegrationsReport {
87
87
  bucket.byStatus[statusKey] = (bucket.byStatus[statusKey] ?? 0) + 1;
88
88
  }
89
89
 
90
- // Additive usage columns (ADR-011). Read per-type totals for each
90
+ // Additive usage columns. Read per-type totals for each
91
91
  // canonical counter; a type with no rows reads as 0. Guarded so a usage
92
92
  // store failure never breaks the structural report.
93
93
  if (this.usageRepository) {
@@ -83,6 +83,13 @@ context only.
83
83
  Request URLs are redacted (query string + userinfo stripped) before they touch a
84
84
  span, so credentials in query params never leak.
85
85
 
86
+ > **Usage-attribution boundary.** The OTel metrics above fire for *every* seam
87
+ > invocation. The durable per-integration **usage** rollup, though, only counts
88
+ > emissions that carry an integration context — set by the handler seams. Requests
89
+ > an API module makes *before an integration exists* (OAuth/token exchange, entity
90
+ > discovery during connection setup) are observable in traces but not attributed to
91
+ > an `api.requests` usage counter (there is no integration to attribute them to).
92
+
86
93
  ## Custom metrics (integration code)
87
94
 
88
95
  Every integration instance carries `this.telemetry` (auto-tagged with its
@@ -213,6 +220,11 @@ await frigg.usage.northStar({ integrationType: 'hubspot', since: daysAgo(30) });
213
220
  Or read it like any counter once you know the key:
214
221
  `frigg.usage.totals({ metric: 'contacts_synced' })`; trends via `frigg.usage.series({ metric })`.
215
222
 
223
+ > `northStar` config is injected at the composition root — the caller that owns
224
+ > the app definition passes it in: `createFriggCommands({ integrationClass, northStar })`
225
+ > (the application layer never reaches up to load it). Omitted → `northStar()`
226
+ > returns `null`.
227
+
216
228
  ## Plugin / extension tap
217
229
 
218
230
  Telemetry flows onto an internal event stream (independent of OTel export, so
@@ -280,19 +292,23 @@ frigg.usage.totals / series ◄── UsageCounter store ──► reporting usa
280
292
  - **Flush is Lambda-safe:** `create-handler` awaits a bounded `forceFlush` in a
281
293
  `finally` (background timers can't fire once the container freezes).
282
294
  - **Usage accuracy is approximate:** at-least-once delivery means a retried
283
- handler could double-count; obvious SQS redeliveries (`ApproximateReceiveCount
284
- > 1`) are discarded.
295
+ handler could double-count. The invocation buffer is discarded only when the
296
+ **whole** SQS batch is a redelivery (`ApproximateReceiveCount > 1`); a mixed
297
+ batch flushes so a redelivered sibling never drops a fresh record's counts.
285
298
 
286
299
  ## Caveats / current limitations
287
300
 
288
301
  - **Usage persistence requires a DB-connected handler.** DB-free handlers (e.g.
289
302
  the raw webhook-receipt route) can't write; `webhooks.received` is counted at
290
303
  the DB-connected `ON_WEBHOOK` queue dispatch instead.
291
- - **DocumentDB** usage adapter inherits the Mongo (Prisma) implementation and is
292
- not yet verified against a real cluster.
304
+ - **DocumentDB** uses a raw-command adapter (`$runCommandRaw`) for increment and
305
+ aggregate; command shapes are unit-tested but not yet run against a real cluster.
293
306
  - **Retention:** the `UsageCounter` table has no pruning yet — hour-grain rows
294
- accumulate. Add a scheduled prune for high-volume deployments.
295
- - Metric `value` is a 32-bit int per `(integrationId, metric, window)` row.
307
+ accumulate. Add a scheduled prune for high-volume deployments. (Read paths are
308
+ covered by composite indexes `(metric, window)` and `(metric, integrationType,
309
+ window)`.)
310
+ - Metric `value` is a `BigInt` per `(integrationId, integrationType, metric,
311
+ window)` row; reads coerce the sum to a JS Number (safe below 2^53).
296
312
 
297
313
  ## See also
298
314
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Wrap a telemetry service so an integration instance's emissions automatically
3
- * carry its `integration_type` (ADR-011). This lets developers write
3
+ * carry its `integration_type`. This lets developers write
4
4
  * `this.telemetry.count('records.synced', n, { entity })` with no per-call
5
5
  * boilerplate, while the usage rollup still attributes the counter to the right
6
6
  * integration type.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Canonical usage-counter vocabulary (ADR-011 Usage-Counter Contract §1).
2
+ * Canonical usage-counter vocabulary.
3
3
  *
4
4
  * These core-owned, versioned keys are the ONLY metrics guaranteed comparable
5
5
  * *across* integration types — they power ADR-010's apples-to-apples comparison
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Builds OpenTelemetry span/metric exporters from an app-definition descriptor
3
- * (ADR-011 Decision 1). All OTel packages are lazy-`require`d here so the no-op
2
+ * Builds OpenTelemetry span/metric exporters from an app-definition descriptor.
3
+ * All OTel packages are lazy-`require`d here so the no-op
4
4
  * path (which never calls this) loads zero OTel modules.
5
5
  *
6
6
  * Supported descriptor `type`s:
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Wraps a single integration handler invocation with a span + the
3
- * `frigg.handler.invocations` counter (ADR-011 Decision 2). Shared by the two
3
+ * `frigg.handler.invocations` counter. Shared by the two
4
4
  * dispatch seams — `IntegrationBase.send()` and `IntegrationEventDispatcher` —
5
5
  * so both paths are instrumented identically.
6
6
  *
@@ -8,7 +8,7 @@ const {
8
8
  * No-op telemetry implementation.
9
9
  *
10
10
  * This is the default when no OTel exporter is configured, so telemetry "rides
11
- * for free" (ADR-011): integration code can call `this.telemetry.*`
11
+ * for free": integration code can call `this.telemetry.*`
12
12
  * unconditionally and it costs nothing on the OTel side. Critically, this module
13
13
  * imports **zero** OpenTelemetry packages — the no-op path never loads the OTel
14
14
  * SDK (guarded by a require-graph test), protecting Lambda cold-start.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Adopter North Star metric (ADR-011 Decision 5). A North Star simply references
2
+ * Adopter North Star metric. A North Star simply references
3
3
  * a usage-counter key (canonical or custom); its value is then read like any
4
4
  * other counter via `frigg.usage.totals({ metric: northStarKey })`. It is
5
5
  * populated either by (a) direct emission of that counter, or (b) derived from a
@@ -47,7 +47,7 @@ function ensureContextManager() {
47
47
  }
48
48
 
49
49
  /**
50
- * The OpenTelemetry-backed telemetry service (ADR-011 Decision 1). Reached only
50
+ * The OpenTelemetry-backed telemetry service. Reached only
51
51
  * when a real exporter is configured; `createTelemetry` returns the no-op
52
52
  * otherwise. `BatchSpanProcessor` is used uniformly — spans are delivered by the
53
53
  * bounded `forceFlush()` the Lambda handler awaits before the container freezes
@@ -1,8 +1,8 @@
1
1
  const { wireTelemetrySubscribers } = require('./plugin-subscribers');
2
2
 
3
3
  /**
4
- * Process-wide wiring of adopter-declared telemetry subscribers (ADR-011
5
- * Decision 6). Built once per cold start and reused for the life of the Lambda
4
+ * Process-wide wiring of adopter-declared telemetry subscribers. Built once per
5
+ * cold start and reused for the life of the Lambda
6
6
  * container, mirroring the usage-rollup singleton. Independent of the usage
7
7
  * rollup: adopter taps wire even when no integration declares `Definition.usage`.
8
8
  * Never throws — a bad/absent app definition just means no adopter taps.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Declarative plugin/extension telemetry taps (ADR-011 Decision 6).
2
+ * Declarative plugin/extension telemetry taps.
3
3
  *
4
4
  * The built-in usage rollup and North Star derivation subscribe to the telemetry
5
5
  * bus in framework code; this lets an ADOPTER subscribe declaratively from the
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Resolves and validates the telemetry section of an app definition
3
- * (ADR-011 Decisions 1 & 5) into a normalized config the TelemetryService and
3
+ * into a normalized config the TelemetryService and
4
4
  * usage rollup consume: `{ exporter, northStar, sampleRatio }`.
5
5
  *
6
6
  * Exporter default is keyed off STAGE: only a genuinely LOCAL run (STAGE=local)
@@ -1,7 +1,7 @@
1
1
  const { AsyncLocalStorage } = require('node:async_hooks');
2
2
 
3
3
  /**
4
- * Ambient telemetry context (ADR-011). A single process-wide AsyncLocalStorage
4
+ * Ambient telemetry context. A single process-wide AsyncLocalStorage
5
5
  * holds the standard identifiers ({integrationId, integrationType, userId,
6
6
  * version, ...}) for the duration of a handler, set once at the handler seam via
7
7
  * `telemetry.withContext(...)`. Any emission during that async scope — including
@@ -1,6 +1,6 @@
1
1
  /**
2
- * TelemetryEventBus — the internal event stream telemetry flows onto
3
- * (ADR-011 Decision 6). Plugins/extensions and the built-in usage rollup
2
+ * TelemetryEventBus — the internal event stream telemetry flows onto.
3
+ * Plugins/extensions and the built-in usage rollup
4
4
  * subscribe here; it is deliberately independent of OTel export (Decision 7),
5
5
  * so usage counters persist even when the OTel exporter is a no-op.
6
6
  *
@@ -12,7 +12,7 @@ function isNoOpExporter(exporter) {
12
12
  }
13
13
 
14
14
  /**
15
- * Create the vendor-neutral telemetry service (ADR-011 Decision 1).
15
+ * Create the vendor-neutral telemetry service.
16
16
  *
17
17
  * Integration code uses the returned object (`this.telemetry`) and never imports
18
18
  * a backend SDK. When no exporter is configured the service is a pure no-op that
@@ -26,8 +26,8 @@ function isNoOpExporter(exporter) {
26
26
  * @param {object} [options.bus] Event bus to reuse (defaults to a fresh one).
27
27
  */
28
28
  function createTelemetry(options = {}) {
29
- // The internal event stream is always on and independent of OTel export
30
- // (ADR-011 Decision 7): the usage rollup + plugin taps must work even when
29
+ // The internal event stream is always on and independent of OTel export:
30
+ // the usage rollup + plugin taps must work even when
31
31
  // the OTel exporter is a no-op. The bus is pure JS, so this does not load
32
32
  // any OTel module on the no-op path.
33
33
  const bus = options.bus || createTelemetryEventBus();
@@ -2,7 +2,7 @@ const { createTelemetry } = require('./telemetry-service');
2
2
  const { createNoOpTelemetry } = require('./no-op-telemetry');
3
3
 
4
4
  /**
5
- * Process-wide telemetry singleton (ADR-011 P4). Created lazily on first use and
5
+ * Process-wide telemetry singleton. Created lazily on first use and
6
6
  * reused for the life of the Lambda container, so the OTel SDK initialises at
7
7
  * most once per cold start. Building it never throws — any failure (e.g. no app
8
8
  * definition on disk in a unit test) falls back to the no-op so a handler is