Mirror of https://github.com/roostorg/coop-integration-example github.com/roostorg/coop-integration-example
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

first commit

Juan S. Mrad c8e08246

+535
+26
.github/workflows/ci.yml
··· 1 + name: CI 2 + 3 + on: 4 + push: 5 + branches: [main, master] 6 + pull_request: 7 + branches: [main, master] 8 + 9 + jobs: 10 + build: 11 + runs-on: ubuntu-latest 12 + steps: 13 + - uses: actions/checkout@v4 14 + 15 + - name: Setup Node.js 16 + uses: actions/setup-node@v6 17 + with: 18 + node-version-file: '.nvmrc' 19 + check-latest: true 20 + cache: 'npm' 21 + 22 + - name: Install dependencies 23 + run: npm ci 24 + 25 + - name: Build 26 + run: npm run build
+26
.github/workflows/publish.yml
··· 1 + name: Publish Package 2 + 3 + on: 4 + push: 5 + tags: 6 + - 'v*' 7 + 8 + permissions: 9 + id-token: write # Required for OIDC 10 + contents: read 11 + 12 + jobs: 13 + publish: 14 + runs-on: ubuntu-latest 15 + steps: 16 + - uses: actions/checkout@v4 17 + 18 + - uses: actions/setup-node@v6 19 + with: 20 + node-version-file: '.nvmrc' 21 + check-latest: true 22 + cache: 'npm' 23 + registry-url: 'https://registry.npmjs.org' 24 + - run: npm ci 25 + - run: npm run build 26 + - run: npm publish
+36
.gitignore
··· 1 + # Dependencies 2 + node_modules/ 3 + 4 + # Build output 5 + dist/ 6 + 7 + # Environment 8 + .env 9 + .env.* 10 + !.env.example 11 + 12 + # Logs and debug 13 + npm-debug.log* 14 + yarn-debug.log* 15 + yarn-error.log* 16 + .pnpm-debug.log* 17 + 18 + # OS 19 + .DS_Store 20 + Thumbs.db 21 + 22 + # IDE / editor 23 + .idea/ 24 + .vscode/ 25 + *.swp 26 + *.swo 27 + *~ 28 + 29 + # Optional npm cache 30 + .npm 31 + 32 + # Optional eslint cache 33 + .eslintcache 34 + 35 + # TypeScript incremental 36 + *.tsbuildinfo
+1
.nvmrc
··· 1 + 24
+83
README.md
··· 1 + # @roostorg/coop-integration-example 2 + 3 + Example [COOP](https://github.com/roostorg/coop) integration plugin. Reference repository showing how to build a custom integration and signals for use in COOP. 4 + 5 + - **Integration config** – saving and loading per-org config (e.g. “True percentage”) 6 + - **Routing rules** – using the plugin signal in conditions 7 + - **Automated enforcement** – the same signal in enforcement rules 8 + 9 + ## What it provides 10 + 11 + - **Integration:** `COOP_INTEGRATION_EXAMPLE` 12 + - **Signal 1 – Random Signal Selection** (`RANDOM_SIGNAL_SELECTION`): Returns `true` or `false` at random. The probability (0–100%) comes from the org’s integration config (“True percentage”). Set e.g. `70` in Org settings → Integrations; the signal returns `true` about 70% of the time. Use this to test config saving and boolean conditions. 13 + - **Signal 2 – Random Score** (`RANDOM_SCORE`): Returns a random number between 0 and 1. No integration config needed. In the rule builder you set a **threshold** (e.g. `0.5`) and choose **above** or **below**. Use this to test numeric score conditions (e.g. “score ≥ 0.5” or “score < 0.3”). 14 + 15 + ## Install 16 + 17 + **From this repo (development):** 18 + 19 + ```bash 20 + git clone https://github.com/roostorg/coop-integration-example.git 21 + cd coop-integration-example 22 + npm install 23 + npm run build 24 + ``` 25 + 26 + **From npm:** 27 + 28 + ```bash 29 + npm install @roostorg/coop-integration-example 30 + ``` 31 + 32 + ## Configure in COOP 33 + 34 + In your COOP `integrations.config.json` (or `INTEGRATIONS_CONFIG_PATH`), add: 35 + 36 + **Local path (development):** 37 + If you cloned this repo next to your COOP server directory, use a path relative to the server (e.g. from `server/`): 38 + 39 + ```json 40 + { 41 + "integrations": [ 42 + { "package": "../coop-integration-example", "enabled": true } 43 + ] 44 + } 45 + ``` 46 + 47 + **From npm:** 48 + 49 + ```json 50 + { 51 + "integrations": [ 52 + { "package": "@roostorg/coop-integration-example", "enabled": true } 53 + ] 54 + } 55 + ``` 56 + 57 + Restart the COOP server so it loads the plugin. 58 + 59 + ## Use in the app 60 + 61 + 1. **Org settings → Integrations** – you should see “COOP Integration Example”. Open it and set **True percentage (0–100)** (e.g. `70`) for Random Signal Selection. Save. 62 + 2. **Rules (routing or enforcement)** – when adding a condition: 63 + - **Random Signal Selection**: Pick that signal; the condition uses your configured percentage (true/false). 64 + - **Random Score**: Pick “Random Score”, then set a **threshold** (e.g. `0.5`) and choose **above** or **below**. The rule compares the random score to your threshold. 65 + 66 + ## Contract 67 + 68 + This package implements the COOP plugin contract from `@roostorg/types`: 69 + 70 + - **Default export:** `CoopIntegrationPlugin` with `manifest` and `createSignals(context)`. 71 + - **Manifest:** `id`, `name`, `version`, `requiresConfig`, `configurationFields`, `signalTypeIds`, `modelCard` (with required sections `modelDetails` and `technicalIntegration`). 72 + - **createSignals:** Returns two descriptors: 73 + - `RANDOM_SIGNAL_SELECTION`: `run(input)` uses `context.getCredential(orgId)` for true percentage; returns `{ outputType: { scalarType: 'BOOLEAN' }, score: boolean }`. 74 + - `RANDOM_SCORE`: `run()` returns `{ outputType: { scalarType: 'NUMBER' }, score: number }` in [0, 1]; no config. Threshold is set in the rule (above/below). 75 + 76 + ## Publishing 77 + 78 + CI runs on push/PR (build only). To publish to npm: 79 + 80 + 1. Create a [GitHub release](https://github.com/roostorg/coop-integration-example/releases) (tag e.g. `v1.0.1`). The **Publish to npm** workflow runs on release and runs `npm publish --access public`. 81 + 2. Add **NPM_TOKEN** in this repo’s secrets (Settings → Secrets and variables → Actions): an npm [automation token](https://docs.npmjs.com/creating-and-viewing-access-tokens) with publish permission for `@roostorg/coop-integration-example`. 82 + 83 + You can also trigger **Publish to npm** manually from the Actions tab (workflow_dispatch).
+88
package-lock.json
··· 1 + { 2 + "name": "@roostorg/coop-integration-example", 3 + "version": "1.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "@roostorg/coop-integration-example", 9 + "version": "1.0.0", 10 + "license": "ISC", 11 + "devDependencies": { 12 + "@roostorg/types": "^1.1.1", 13 + "typescript": "^5.0.0" 14 + }, 15 + "engines": { 16 + "node": ">=18" 17 + }, 18 + "peerDependencies": { 19 + "@roostorg/types": ">=1.0.0" 20 + } 21 + }, 22 + "node_modules/@babel/runtime": { 23 + "version": "7.28.6", 24 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", 25 + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", 26 + "dev": true, 27 + "license": "MIT", 28 + "engines": { 29 + "node": ">=6.9.0" 30 + } 31 + }, 32 + "node_modules/@roostorg/types": { 33 + "version": "1.1.1", 34 + "resolved": "https://registry.npmjs.org/@roostorg/types/-/types-1.1.1.tgz", 35 + "integrity": "sha512-NhPYlG27wAQaD7AzWkL3LJHu52/QfK8lt9QMahUx7fbRtB4fYILy4fGcLQvt45gNQANoU78evW1UJftAB0B89Q==", 36 + "dev": true, 37 + "license": "ISC", 38 + "dependencies": { 39 + "date-fns": "^2.29.3", 40 + "type-fest": "^4.3.2" 41 + } 42 + }, 43 + "node_modules/date-fns": { 44 + "version": "2.30.0", 45 + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", 46 + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", 47 + "dev": true, 48 + "license": "MIT", 49 + "dependencies": { 50 + "@babel/runtime": "^7.21.0" 51 + }, 52 + "engines": { 53 + "node": ">=0.11" 54 + }, 55 + "funding": { 56 + "type": "opencollective", 57 + "url": "https://opencollective.com/date-fns" 58 + } 59 + }, 60 + "node_modules/type-fest": { 61 + "version": "4.41.0", 62 + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", 63 + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", 64 + "dev": true, 65 + "license": "(MIT OR CC0-1.0)", 66 + "engines": { 67 + "node": ">=16" 68 + }, 69 + "funding": { 70 + "url": "https://github.com/sponsors/sindresorhus" 71 + } 72 + }, 73 + "node_modules/typescript": { 74 + "version": "5.9.3", 75 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 76 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 77 + "dev": true, 78 + "license": "Apache-2.0", 79 + "bin": { 80 + "tsc": "bin/tsc", 81 + "tsserver": "bin/tsserver" 82 + }, 83 + "engines": { 84 + "node": ">=14.17" 85 + } 86 + } 87 + } 88 + }
+45
package.json
··· 1 + { 2 + "name": "@roostorg/coop-integration-example", 3 + "version": "1.0.0", 4 + "description": "Example package to show how a custom integration and signal can be used in COOP this is meant to be a reference repository and provide basic determination", 5 + "type": "module", 6 + "main": "dist/index.js", 7 + "types": "dist/index.d.ts", 8 + "files": [ 9 + "dist", 10 + "README.md", 11 + "roost-example-logo.png", 12 + "roost-example-with-background.png" 13 + ], 14 + "scripts": { 15 + "build": "tsc", 16 + "prepare": "npm run build", 17 + "prepublishOnly": "npm run build" 18 + }, 19 + "keywords": [ 20 + "coop", 21 + "integration", 22 + "plugin", 23 + "example" 24 + ], 25 + "author": "Roostorg", 26 + "license": "apache-2.0", 27 + "repository": { 28 + "type": "git", 29 + "url": "https://github.com/roostorg/coop-integration-example.git" 30 + }, 31 + "bugs": { 32 + "url": "https://github.com/roostorg/coop-integration-example/issues" 33 + }, 34 + "homepage": "https://github.com/roostorg/coop-integration-example#readme", 35 + "peerDependencies": { 36 + "@roostorg/types": ">=1.0.0" 37 + }, 38 + "devDependencies": { 39 + "@roostorg/types": "^1.1.1", 40 + "typescript": "^5.0.0" 41 + }, 42 + "engines": { 43 + "node": ">=18" 44 + } 45 + }
roost-example-logo.png

This is a binary file and will not be displayed.

roost-example-with-background.png

This is a binary file and will not be displayed.

+214
src/index.ts
··· 1 + /** 2 + * Example COOP integration plugin with two signal types: 3 + * 1. Random Signal Selection – boolean, probability from org config (tests config saving). 4 + * 2. Random Score – numeric [0, 1], threshold set in the rule (tests score vs threshold). 5 + */ 6 + 7 + import type { 8 + CoopIntegrationPlugin, 9 + IntegrationManifest, 10 + ModelCard, 11 + PluginSignalContext, 12 + PluginSignalDescriptor, 13 + } from '@roostorg/types'; 14 + 15 + const SIGNAL_TYPE_RANDOM_SELECTION = 'RANDOM_SIGNAL_SELECTION'; 16 + const SIGNAL_TYPE_RANDOM_SCORE = 'RANDOM_SCORE'; 17 + const INTEGRATION_ID = 'COOP_INTEGRATION_EXAMPLE'; 18 + const DEFAULT_TRUE_PERCENTAGE = 50; 19 + 20 + const modelCard: ModelCard = { 21 + modelName: 'COOP Integration Example', 22 + version: '1.0.0', 23 + releaseDate: '2026', 24 + sections: [ 25 + { 26 + id: 'modelDetails', 27 + title: 'Model Details', 28 + fields: [ 29 + { label: 'Model Name', value: 'COOP Integration Example' }, 30 + { 31 + label: 'Purpose', 32 + value: 33 + 'Example plugin with two signals: one uses org config (boolean), one returns a numeric score so you can set a threshold in the rule (over/under).', 34 + }, 35 + { 36 + label: 'Signals', 37 + value: `${SIGNAL_TYPE_RANDOM_SELECTION} (boolean, config-driven) and ${SIGNAL_TYPE_RANDOM_SCORE} (number 0–1, threshold in rule).`, 38 + }, 39 + ], 40 + }, 41 + { 42 + id: 'technicalIntegration', 43 + title: 'Technical Integration', 44 + fields: [ 45 + { 46 + label: 'Signal types', 47 + value: `${SIGNAL_TYPE_RANDOM_SELECTION}, ${SIGNAL_TYPE_RANDOM_SCORE}`, 48 + }, 49 + { 50 + label: 'Config', 51 + value: 'truePercentage (0–100) for Random Signal Selection only; Random Score needs no config.', 52 + }, 53 + ], 54 + }, 55 + ], 56 + }; 57 + 58 + const manifest: IntegrationManifest = { 59 + id: INTEGRATION_ID, 60 + name: 'COOP Integration Example', 61 + version: '1.0.0', 62 + description: 63 + 'Example plugin with two signals: config-driven boolean and a numeric score you compare with a threshold in the rule.', 64 + docsUrl: 'https://github.com/roostorg/coop/tree/main/coop-integration-example', 65 + requiresConfig: true, 66 + configurationFields: [ 67 + { 68 + key: 'truePercentage', 69 + label: 'True percentage (0–100)', 70 + required: true, 71 + inputType: 'text', 72 + placeholder: '50', 73 + description: 74 + 'Used by Random Signal Selection only. Probability (0–100) that it returns true. Default 50 if not set.', 75 + }, 76 + ], 77 + signalTypeIds: [SIGNAL_TYPE_RANDOM_SELECTION, SIGNAL_TYPE_RANDOM_SCORE], 78 + modelCard, 79 + logoPath: 'roost-example-logo.png', 80 + logoWithBackgroundPath: 'roost-example-with-background.png', 81 + }; 82 + 83 + /** Parses truePercentage from org config; returns 0–100, default 50 if missing or invalid. */ 84 + function parseTruePercentage(config: Record<string, unknown>): number { 85 + const v = config.truePercentage; 86 + if (v === undefined || v === null) return DEFAULT_TRUE_PERCENTAGE; 87 + const n = typeof v === 'number' ? v : Number(String(v).trim()); 88 + if (!Number.isFinite(n)) return DEFAULT_TRUE_PERCENTAGE; 89 + return Math.max(0, Math.min(100, n)); 90 + } 91 + 92 + function hasTruePercentageConfig(config: Record<string, unknown> | null | undefined): boolean { 93 + if (config == null) return false; 94 + const v = (config as { truePercentage?: unknown }).truePercentage; 95 + return v !== undefined && v !== null && String(v).trim() !== ''; 96 + } 97 + 98 + function createRandomSignalSelectionDescriptor( 99 + context: PluginSignalContext, 100 + ): PluginSignalDescriptor { 101 + const { integrationId, getCredential } = context; 102 + const outputType = { scalarType: 'BOOLEAN' as const }; 103 + 104 + return { 105 + id: { type: SIGNAL_TYPE_RANDOM_SELECTION }, 106 + displayName: 'Coin Flip Selection', 107 + description: 108 + 'Returns true or false at random, with a configurable probability (true percentage 0–100) from the integration config.', 109 + docsUrl: null, 110 + recommendedThresholds: { 111 + highPrecisionThreshold: 0.5, 112 + highRecallThreshold: 0.5, 113 + }, 114 + supportedLanguages: 'ALL', 115 + pricingStructure: { type: 'FREE' }, 116 + eligibleInputs: ['STRING', 'IMAGE', 'FULL_ITEM'], 117 + outputType, 118 + getCost: () => 0, 119 + needsMatchingValues: false, 120 + eligibleSubcategories: [], 121 + needsActionPenalties: false, 122 + integration: integrationId, 123 + allowedInAutomatedRules: true, 124 + 125 + async run(input: unknown): Promise<{ outputType: typeof outputType; score: boolean }> { 126 + const orgId = (input as { orgId?: string })?.orgId; 127 + if (typeof orgId !== 'string') { 128 + return { outputType, score: Math.random() < DEFAULT_TRUE_PERCENTAGE / 100 }; 129 + } 130 + const config = await getCredential(orgId); 131 + const truePct = parseTruePercentage(config ?? {}); 132 + const score = Math.random() * 100 < truePct; 133 + // Because outputType is { scalarType: 'BOOLEAN' }, Coop will use the output score as is for the condition. 134 + return { outputType, score }; 135 + }, 136 + 137 + async getDisabledInfo(orgId: string) { 138 + const config = await getCredential(orgId); 139 + if (hasTruePercentageConfig(config ?? undefined)) { 140 + return { disabled: false }; 141 + } 142 + return { 143 + disabled: true, 144 + disabledMessage: 145 + 'Configure the integration (True percentage 0–100) in Org settings to use this signal.', 146 + }; 147 + }, 148 + }; 149 + } 150 + 151 + function createRandomScoreDescriptor( 152 + context: PluginSignalContext, 153 + ): PluginSignalDescriptor { 154 + const { integrationId } = context; 155 + const outputType = { scalarType: 'NUMBER' as const }; 156 + 157 + return { 158 + id: { type: SIGNAL_TYPE_RANDOM_SCORE }, 159 + displayName: 'Random Score', 160 + description: 161 + 'Returns a random number between 0 and 1. Set a threshold in the rule (e.g. 0.5) and choose "above" or "below" to test numeric conditions.', 162 + docsUrl: null, 163 + recommendedThresholds: { 164 + highPrecisionThreshold: 0.5, 165 + highRecallThreshold: 0.5, 166 + }, 167 + supportedLanguages: 'ALL', 168 + pricingStructure: { type: 'FREE' }, 169 + eligibleInputs: ['STRING', 'IMAGE', 'FULL_ITEM'], 170 + outputType, 171 + getCost: () => 0, 172 + needsMatchingValues: false, 173 + eligibleSubcategories: [], 174 + needsActionPenalties: false, 175 + integration: integrationId, 176 + allowedInAutomatedRules: true, 177 + 178 + async run( 179 + _input: unknown, 180 + ): Promise<{ outputType: typeof outputType; score: number }> { 181 + // Returns a random number between 0 and 100. 182 + // Because outputType is { scalarType: 'NUMBER' }, Coop can take the score and compare it to a threshold in the rule. 183 + const score = Math.random() * 100; 184 + return { outputType, score }; 185 + }, 186 + 187 + async getDisabledInfo() { 188 + return { disabled: false }; 189 + }, 190 + }; 191 + } 192 + 193 + function createSignals( 194 + context: PluginSignalContext, 195 + ): ReadonlyArray<{ signalTypeId: string; signal: PluginSignalDescriptor }> { 196 + return [ 197 + { 198 + signalTypeId: SIGNAL_TYPE_RANDOM_SELECTION, 199 + signal: createRandomSignalSelectionDescriptor(context), 200 + }, 201 + { 202 + signalTypeId: SIGNAL_TYPE_RANDOM_SCORE, 203 + signal: createRandomScoreDescriptor(context), 204 + }, 205 + ]; 206 + } 207 + 208 + const plugin: CoopIntegrationPlugin = { 209 + manifest, 210 + createSignals, 211 + }; 212 + 213 + export default plugin; 214 + export { manifest, createSignals };
+16
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "NodeNext", 5 + "moduleResolution": "NodeNext", 6 + "outDir": "./dist", 7 + "rootDir": "./src", 8 + "declaration": true, 9 + "strict": true, 10 + "skipLibCheck": true, 11 + "esModuleInterop": true, 12 + "forceConsistentCasingInFileNames": true 13 + }, 14 + "include": ["src/**/*.ts"], 15 + "exclude": ["node_modules", "dist"] 16 + }