@geonosis/integrations 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +211 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +142 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# `@geonosis/integrations`
|
|
2
|
+
|
|
3
|
+
One integration seam: a prefixed id, an optional settings schema, and a registry that refuses a
|
|
4
|
+
duplicate id, an id outside its prefix, or a missing required id — at build time, once, instead of
|
|
5
|
+
on every lookup for the life of the process.
|
|
6
|
+
|
|
7
|
+
Zero runtime dependencies. It never imports zod.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @geonosis/integrations
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
dielime arrived at the same seam three times, with no shared code between them:
|
|
16
|
+
`AbstractCrmProvider` + `buildCrmProviderRegistry` (`cp_*`), `AbstractSearchProvider` +
|
|
17
|
+
`buildSearchProviderRegistry` (`sp_*`), and `AbstractMessagingChannelProvider` +
|
|
18
|
+
`buildMessagingProviderRegistry` (`mp_*`). Midday is the same story one stage later: **five**
|
|
19
|
+
incompatible plugin seams for one concept, a `UnifiedApp` manifest with 25 optional fields and
|
|
20
|
+
`value: any`, a settings UI hand-mapped inside a dashboard component, and 8 of 35 integration
|
|
21
|
+
directories reachable from nothing.
|
|
22
|
+
|
|
23
|
+
The measured gain is smaller and sharper than "one shape for all of them". dielime declares
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
export const CRM_PROVIDER_PREFIX = 'cp_' // modules/crm/types.ts:145
|
|
27
|
+
export const SEARCH_PROVIDER_PREFIX = 'sp_' // modules/search/types.ts:135
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
and **references neither anywhere else in the repo**. Both registries are a
|
|
31
|
+
`Record<string, Provider>` built from an object literal, so:
|
|
32
|
+
|
|
33
|
+
- a key that ignores the prefix is accepted;
|
|
34
|
+
- a second entry under an id already taken silently replaces the first, and the one that disappears
|
|
35
|
+
is whichever was written higher up;
|
|
36
|
+
- "the configured default must be registered" is checked inside `getActiveProvider()` — on every
|
|
37
|
+
call, forever — instead of once, when the registry is built.
|
|
38
|
+
|
|
39
|
+
Two dead constants and three invariants stated in comments and held by nothing. This package holds
|
|
40
|
+
them.
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### `defineIntegration({ id, settingsSchema?, health?, isConfigured? })`
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { defineIntegration } from '@geonosis/integrations'
|
|
48
|
+
import { z } from 'zod'
|
|
49
|
+
|
|
50
|
+
export const attio = defineIntegration({
|
|
51
|
+
id: 'cp_attio',
|
|
52
|
+
settingsSchema: z.object({
|
|
53
|
+
apiKey: z.string().describe('The Attio API key'),
|
|
54
|
+
baseUrl: z.string().optional(),
|
|
55
|
+
}),
|
|
56
|
+
health: () => client.ping(),
|
|
57
|
+
isConfigured: () => apiKey !== undefined,
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Refuses an empty or non-string `id`, a `settingsSchema` with no `safeParse`, and a `health` or
|
|
62
|
+
`isConfigured` that is not callable.
|
|
63
|
+
|
|
64
|
+
`health` and `isConfigured` are the two probes `AbstractCrmProvider` and `AbstractSearchProvider`
|
|
65
|
+
both declare. Everything a provider does beyond them — `upsertContact`, `search`, `parseInbound` —
|
|
66
|
+
is the consumer's own contract and stays there; this package holds the seam, not the work.
|
|
67
|
+
|
|
68
|
+
### `createIntegrationRegistry({ prefix, integrations, requiredId? })`
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const crm = createIntegrationRegistry({
|
|
72
|
+
prefix: 'cp_',
|
|
73
|
+
integrations: [attio, hubspot],
|
|
74
|
+
requiredId: 'cp_attio',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
crm.get('cp_attio') // throws, naming what IS registered, if absent
|
|
78
|
+
crm.has('cp_hubspot') // boolean, no throw
|
|
79
|
+
crm.list() // declaration order
|
|
80
|
+
crm.required() // the `requiredId` integration
|
|
81
|
+
crm.prefix // 'cp_'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Refuses, when built: an empty `prefix`; an entry that never went through `defineIntegration`; an id
|
|
85
|
+
outside the prefix; a duplicate id; a `requiredId` outside the prefix or absent from the list.
|
|
86
|
+
|
|
87
|
+
`requiredId` is dielime's `DEFAULT_CRM_PROVIDER_ID` / `DEFAULT_SEARCH_PROVIDER_ID` plus the
|
|
88
|
+
`NOT_FOUND` its `getActiveProvider()` throws — moved to the one moment the question can be answered
|
|
89
|
+
once. (Plan 022 called this option `manualImplementationRequired`; it is named for the consumer
|
|
90
|
+
construct it extracts.)
|
|
91
|
+
|
|
92
|
+
### `settingsFieldsOf(integration)`
|
|
93
|
+
|
|
94
|
+
A JSON description of the settings schema's fields, so a settings form is **derived** rather than
|
|
95
|
+
hand-mapped:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
settingsFieldsOf(attio)
|
|
99
|
+
// [ { name: 'apiKey', type: 'string', required: true, description: 'The Attio API key' },
|
|
100
|
+
// { name: 'baseUrl', type: 'string', required: false } ]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`optional`, `default` and `prefault` make a field not required; `nullable` and `readonly` are peeled
|
|
104
|
+
without changing that; an enum carries its choices as `options`. An integration with no schema, or
|
|
105
|
+
a schema that is not an object of fields, has no fields. A schema this reader **cannot see into**
|
|
106
|
+
throws rather than returning an empty list — a blank settings page with no explanation is the silent
|
|
107
|
+
zero this kit refuses everywhere else.
|
|
108
|
+
|
|
109
|
+
## The one shape this package does not replace
|
|
110
|
+
|
|
111
|
+
dielime's third seam is not this shape and is deliberately out of scope:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
buildMessagingProviderRegistry(): Record<CommunicationChannel, AbstractMessagingChannelProvider | undefined>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
It is keyed by `CommunicationChannel`, not by a prefixed id — the `mp_*` ids live in a separate
|
|
118
|
+
`MESSAGING_PROVIDER_IDS` map the registry never uses — and it is **total with holes**: `sms`,
|
|
119
|
+
`telegram` and `webchat` are present and `undefined`, so a new channel breaks the build until
|
|
120
|
+
someone answers for it. Its own test asserts exactly that:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
expect(reg.sms).toBeUndefined() // providers.test.ts:157
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
A registry that refuses a non-integration destroys the totality that test protects. That is a
|
|
127
|
+
second registry shape — keyed and total over a declared union — and replacing it with this one would
|
|
128
|
+
be a downgrade. It is the only registry test either consumer has, which is why it is quoted here
|
|
129
|
+
rather than summarised.
|
|
130
|
+
|
|
131
|
+
## Migration — dielime, five lines
|
|
132
|
+
|
|
133
|
+
```diff
|
|
134
|
+
-export function buildCrmProviderRegistry(opts: CrmProviderOptions = {}): Record<string, AbstractCrmProvider> {
|
|
135
|
+
- return { [DEFAULT_CRM_PROVIDER_ID]: new AttioCrmProviderService(...), [CRM_PROVIDER_IDS.hubspot]: new HubspotCrmProviderService(...) }
|
|
136
|
+
-}
|
|
137
|
+
+export const buildCrmProviderRegistry = (opts: CrmProviderOptions = {}) =>
|
|
138
|
+
+ createIntegrationRegistry({
|
|
139
|
+
+ prefix: CRM_PROVIDER_PREFIX,
|
|
140
|
+
+ requiredId: DEFAULT_CRM_PROVIDER_ID,
|
|
141
|
+
+ integrations: [attioIntegration(opts), hubspotIntegration(opts)],
|
|
142
|
+
+ })
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`getActiveProvider()` becomes `registry.required()`, and its `NOT_FOUND` throw is deleted — the
|
|
146
|
+
registry already refused to exist. `CRM_PROVIDER_PREFIX` stops being a dead export.
|
|
147
|
+
|
|
148
|
+
## Migration — during.day, five lines
|
|
149
|
+
|
|
150
|
+
`apps/web/features/apps/stores/apps-store.ts` is a hardcoded array of 12 `AppDefinition`s
|
|
151
|
+
(`{ category, description, icon, id, name }`) with client-only `installedIds` and no server half.
|
|
152
|
+
Its first real integration is where the seam is worth having:
|
|
153
|
+
|
|
154
|
+
```diff
|
|
155
|
+
-const DEFAULT_APPS: AppDefinition[] = [ { id: 'slack', name: 'Slack', ... }, ... ]
|
|
156
|
+
+export const apps = createIntegrationRegistry({
|
|
157
|
+
+ prefix: 'app_',
|
|
158
|
+
+ integrations: [slack, gmail, outlook],
|
|
159
|
+
+})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The presentation fields (`category`, `description`, `icon`, `name`) stay in during.day: they are a
|
|
163
|
+
UI catalogue, and this package deliberately carries no manifest. See below.
|
|
164
|
+
|
|
165
|
+
## Equivalence status (D-027)
|
|
166
|
+
|
|
167
|
+
| Piece | Status |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `createIntegrationRegistry` — prefix, duplicates, `requiredId` | **extracted** — three consumer copies, the invariants they state and do not hold |
|
|
170
|
+
| `defineIntegration` — `id`, `health`, `isConfigured` | **extracted** — the fields two of the three abstractions declare |
|
|
171
|
+
| `settingsSchema` + `settingsFieldsOf` | **from Midday's failure, no consumer fixture yet** — neither consumer holds a settings schema today; the field exists because `value: any` plus a hand-mapped dashboard component is where its absence leads |
|
|
172
|
+
| manifest (`name`, `category`, `description`, `logo`) | **not here** — Midday's is 25 optional fields, and neither consumer's provider carries one |
|
|
173
|
+
| install lifecycle (`installUrl`, `onCallback`, `onUninstall`) | **not here** — zero occurrences in either consumer. dielime's integrations are credential-configured; during.day's have no server half. It waits for a consumer that holds one |
|
|
174
|
+
| the channel-keyed registry | **not replaced** — see above |
|
|
175
|
+
|
|
176
|
+
**Adopted by:** nobody yet. This is a published package with contract tests over both consumers'
|
|
177
|
+
measured shapes, not a migration that has happened.
|
|
178
|
+
|
|
179
|
+
## zod, and why the guard is structural
|
|
180
|
+
|
|
181
|
+
`settingsSchema` is typed structurally — `{ safeParse }` — and checked the same way. This package
|
|
182
|
+
never imports zod, and has no runtime dependencies at all. The declared peer is `^4.0.0`, optional.
|
|
183
|
+
|
|
184
|
+
The reason, measured rather than assumed:
|
|
185
|
+
|
|
186
|
+
| | dielime plugins | dielime root | during.day |
|
|
187
|
+
|---|---|---|---|
|
|
188
|
+
| import specifier | `@medusajs/framework/zod` | `zod` | `zod` (catalog) |
|
|
189
|
+
| resolved version | **4.2.0** (via `@medusajs/deps`) | 4.4.3 | 4.5.4 |
|
|
190
|
+
|
|
191
|
+
Three module instances of one major, across two consumers, with genuinely different constructors
|
|
192
|
+
and prototypes.
|
|
193
|
+
|
|
194
|
+
The obvious conclusion from that — "so an `instanceof z.ZodType` guard would refuse dielime's
|
|
195
|
+
schemas" — **is wrong for zod 4, and this package's tests are where that was found out.** Zod 4 puts
|
|
196
|
+
a `Symbol.hasInstance` on its classes that answers by an internal trait rather than by the prototype
|
|
197
|
+
chain, so `instanceof` bridges copies of zod 4 fine. It does **not** bridge zod 3, whose schemas are
|
|
198
|
+
plain classes — and dielime's own root package declares its zod peer as `^3.0.0 || ^4.0.0`.
|
|
199
|
+
|
|
200
|
+
So the structural guard is still the right one, for the reasons that survived the measurement: it
|
|
201
|
+
asks what a schema does, which every copy and every major answers identically; and it needs no
|
|
202
|
+
import, which keeps this package out of the version disagreement entirely. `settingsFieldsOf` reads
|
|
203
|
+
zod 4 internals and says so loudly when handed something else.
|
|
204
|
+
|
|
205
|
+
`src/realms.test.ts` asserts all of it against three real copies of zod (4.5, 4.2 and 3.25) in one
|
|
206
|
+
process, starting with the assertion that they are genuinely different modules — without which the
|
|
207
|
+
rest would pass and prove nothing.
|
|
208
|
+
|
|
209
|
+
## Licence
|
|
210
|
+
|
|
211
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One integration seam, carrying what the consumers actually hold.
|
|
3
|
+
*
|
|
4
|
+
* dielime arrived at the same shape three times — `AbstractCrmProvider` (`cp_*`),
|
|
5
|
+
* `AbstractSearchProvider` (`sp_*`) and `AbstractMessagingChannelProvider` (`mp_*`), each with its
|
|
6
|
+
* own registry builder and no shared code. Two of the three declare `health()` and `isConfigured()`;
|
|
7
|
+
* all three key on an id. Those are the fields below that are extraction.
|
|
8
|
+
*
|
|
9
|
+
* `settingsSchema` is the one field taken from a failure rather than from a consumer. Midday's
|
|
10
|
+
* `UnifiedApp` carries a 25-optional-field manifest whose `settings` are data typed `value: any`,
|
|
11
|
+
* and the UI that renders them is a hardcoded map inside a dashboard component, with a config type
|
|
12
|
+
* that covers 6 of its 27 apps. A schema instead of an array is what lets the form be derived —
|
|
13
|
+
* see `settingsFieldsOf` — so the validator and the UI cannot disagree.
|
|
14
|
+
*
|
|
15
|
+
* There is deliberately no install lifecycle here. `installUrl`, `onCallback` and `onUninstall`
|
|
16
|
+
* appear nowhere in either consumer: dielime's integrations are credential-configured, and
|
|
17
|
+
* during.day's are a hardcoded array in a Zustand store with no server half at all. They wait for
|
|
18
|
+
* a consumer that holds one.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* A schema, recognised by what it does rather than by what it is an instance of.
|
|
22
|
+
*
|
|
23
|
+
* This is not squeamishness about types. dielime imports `z` from `@medusajs/framework/zod` under a
|
|
24
|
+
* repo rule forbidding bare `zod`, and that resolves to the copy `@medusajs/deps` ships (4.2.0),
|
|
25
|
+
* while its own root devDependency is 4.4.3 and during.day's catalog is 4.5.4 — three module
|
|
26
|
+
* instances of one major, across two consumers. `value instanceof z.ZodType` compares against a
|
|
27
|
+
* class object, and a schema from another copy of zod is not an instance of ours. Such a guard
|
|
28
|
+
* passes every test written against the kit's own zod and refuses every schema dielime hands it.
|
|
29
|
+
*/
|
|
30
|
+
type SchemaLike = {
|
|
31
|
+
safeParse: (value: unknown) => unknown;
|
|
32
|
+
};
|
|
33
|
+
type Integration<Id extends string = string> = {
|
|
34
|
+
health?: () => Promise<boolean>;
|
|
35
|
+
id: Id;
|
|
36
|
+
isConfigured?: () => boolean;
|
|
37
|
+
settingsSchema?: SchemaLike;
|
|
38
|
+
};
|
|
39
|
+
type IntegrationInput<Id extends string = string> = Integration<Id>;
|
|
40
|
+
/** Whether this object went through `defineIntegration` rather than being written out by hand. */
|
|
41
|
+
declare const isIntegration: (value: unknown) => value is Integration;
|
|
42
|
+
declare const defineIntegration: <const Id extends string>(input: IntegrationInput<Id>) => Integration<Id>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The registry both prefixed-id seams already have, with the invariants they state and do not hold.
|
|
46
|
+
*
|
|
47
|
+
* dielime declares `CRM_PROVIDER_PREFIX = 'cp_'` in `modules/crm/types.ts` and
|
|
48
|
+
* `SEARCH_PROVIDER_PREFIX = 'sp_'` in `modules/search/types.ts`, and references neither anywhere
|
|
49
|
+
* else in the repo — two exports that exist to document a convention nothing enforces. Both
|
|
50
|
+
* registries are a `Record<string, Provider>` built from an object literal, so a key that ignores
|
|
51
|
+
* the prefix is accepted, a second entry under an id already taken silently replaces the first, and
|
|
52
|
+
* "the configured default must be registered" is checked inside `getActiveProvider()` on every
|
|
53
|
+
* call, for the life of the process, instead of once when the registry is built.
|
|
54
|
+
*
|
|
55
|
+
* Those three are what this holds. It is not a different design; it is the design the comments
|
|
56
|
+
* already describe, made refusable.
|
|
57
|
+
*
|
|
58
|
+
* It covers ONE of the three seams' shapes: lookup by prefixed id. The messaging registry is keyed
|
|
59
|
+
* by `CommunicationChannel` and holds `undefined` for the channels it has not built — a totality
|
|
60
|
+
* over a declared union, which its own test asserts. That is a second shape, and this package does
|
|
61
|
+
* not replace it.
|
|
62
|
+
*/
|
|
63
|
+
type RegistryInput<Id extends string = string> = {
|
|
64
|
+
integrations: readonly Integration<Id>[];
|
|
65
|
+
prefix: string;
|
|
66
|
+
/**
|
|
67
|
+
* The id that must be present for the registry to be worth building — dielime spells it
|
|
68
|
+
* `DEFAULT_CRM_PROVIDER_ID` / `DEFAULT_SEARCH_PROVIDER_ID`, and its absence is a `NOT_FOUND`
|
|
69
|
+
* thrown on first use rather than a refusal to start.
|
|
70
|
+
*/
|
|
71
|
+
requiredId?: Id;
|
|
72
|
+
};
|
|
73
|
+
type IntegrationRegistry<Id extends string = string> = {
|
|
74
|
+
get: (id: string) => Integration<Id>;
|
|
75
|
+
has: (id: string) => boolean;
|
|
76
|
+
list: () => readonly Integration<Id>[];
|
|
77
|
+
prefix: string;
|
|
78
|
+
required: () => Integration<Id>;
|
|
79
|
+
};
|
|
80
|
+
declare const createIntegrationRegistry: <const Id extends string>(input: RegistryInput<Id>) => IntegrationRegistry<Id>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The settings form, read off the schema that already validates the settings.
|
|
84
|
+
*
|
|
85
|
+
* Midday routes an app's OAuth start through a hardcoded endpoint map inside a dashboard component,
|
|
86
|
+
* hand-writes a React component per MCP client for its setup instructions, and types its stored
|
|
87
|
+
* config with a conditional map covering 6 of 27 apps. Every one of those is the same act: a fact
|
|
88
|
+
* about an integration, restated somewhere the integration cannot see. Deriving the fields is how
|
|
89
|
+
* the restatement stops being possible.
|
|
90
|
+
*
|
|
91
|
+
* This returns a description, not a UI. What a `string` field looks like is the consumer's
|
|
92
|
+
* business; that there IS a required string called `apiKey` is the schema's.
|
|
93
|
+
*/
|
|
94
|
+
type SettingsField = {
|
|
95
|
+
description?: string;
|
|
96
|
+
name: string;
|
|
97
|
+
options?: readonly string[];
|
|
98
|
+
required: boolean;
|
|
99
|
+
type: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The fields, or none — but never none because the schema could not be read.
|
|
103
|
+
*
|
|
104
|
+
* An integration with no settings schema, and one whose schema is a `string` rather than an object
|
|
105
|
+
* of fields, both have nothing to render, and an empty list is the honest answer for both. A schema
|
|
106
|
+
* this reader cannot see INTO is a different thing entirely: zod 3 keeps its shape behind `_def`
|
|
107
|
+
* and spells it as a function, so a zod 3 object would come back with no fields and render a
|
|
108
|
+
* settings page that is blank for a reason nothing on it explains. That is the silent zero a
|
|
109
|
+
* counter reports under a format it cannot parse, wearing a form's clothes, so it is refused.
|
|
110
|
+
*/
|
|
111
|
+
declare const settingsFieldsOf: (integration: Integration) => SettingsField[];
|
|
112
|
+
|
|
113
|
+
export { type Integration, type IntegrationInput, type IntegrationRegistry, type RegistryInput, type SchemaLike, type SettingsField, createIntegrationRegistry, defineIntegration, isIntegration, settingsFieldsOf };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/define.ts
|
|
2
|
+
var DECLARED = /* @__PURE__ */ Symbol.for("geonosis.integrations.declared");
|
|
3
|
+
var isIntegration = (value) => typeof value === "object" && value !== null && value[DECLARED] === true;
|
|
4
|
+
var requireCallable = (value, field, id) => {
|
|
5
|
+
if (value !== void 0 && typeof value !== "function") {
|
|
6
|
+
throw new TypeError(
|
|
7
|
+
`defineIntegration("${id}"): \`${field}\` must be a function, and is ${typeof value}. A probe that cannot be called is a probe nobody runs.`
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var defineIntegration = (input) => {
|
|
12
|
+
const { health, id, isConfigured, settingsSchema } = input;
|
|
13
|
+
if (typeof id !== "string" || id === "") {
|
|
14
|
+
throw new TypeError(
|
|
15
|
+
"defineIntegration: `id` must be a non-empty string. An integration with no id cannot be registered, looked up, or named in a diagnostic."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (settingsSchema !== void 0 && typeof settingsSchema.safeParse !== "function") {
|
|
19
|
+
throw new TypeError(
|
|
20
|
+
`defineIntegration("${id}"): \`settingsSchema\` must expose \`safeParse\`. A settings description that cannot validate anything is Midday's \`value: any\` with a schema-shaped name on it \u2014 the settings UI is generated from this, so nothing else can stand in for it.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
requireCallable(health, "health", id);
|
|
24
|
+
requireCallable(isConfigured, "isConfigured", id);
|
|
25
|
+
return { ...input, [DECLARED]: true };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/registry.ts
|
|
29
|
+
var prefixed = (id, prefix) => id.startsWith(prefix);
|
|
30
|
+
var createIntegrationRegistry = (input) => {
|
|
31
|
+
const { integrations, prefix, requiredId } = input;
|
|
32
|
+
if (typeof prefix !== "string" || prefix === "") {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
"createIntegrationRegistry: `prefix` must be a non-empty string. A prefix that is nothing enforces nothing, and a check that cannot fail reads exactly like a check that found nothing \u2014 which is how both consumers ended up with a prefix constant referenced by no code."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const byId = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const integration of integrations ?? []) {
|
|
39
|
+
if (!isIntegration(integration)) {
|
|
40
|
+
throw new TypeError(
|
|
41
|
+
`createIntegrationRegistry("${prefix}"): every entry must come from \`defineIntegration\`. An object literal skips the checks on the id, the settings schema and the probes, which is the hand-written manifest this package exists to replace.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (!prefixed(integration.id, prefix)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`createIntegrationRegistry("${prefix}"): "${integration.id}" does not carry the prefix "${prefix}". The prefix is the registry's namespace; an id outside it collides with whatever else shares the container.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (byId.has(integration.id)) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`createIntegrationRegistry("${prefix}"): "${integration.id}" is registered twice. In an object literal the second entry silently replaces the first, and the integration that disappears is whichever was written higher up.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
byId.set(integration.id, integration);
|
|
55
|
+
}
|
|
56
|
+
if (requiredId !== void 0) {
|
|
57
|
+
if (!prefixed(requiredId, prefix)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`createIntegrationRegistry("${prefix}"): the required id "${requiredId}" does not carry the prefix "${prefix}".`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!byId.has(requiredId)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`createIntegrationRegistry("${prefix}"): the required id "${requiredId}" is not registered. It is the one integration this registry is not worth building without, so it is refused here rather than on the first lookup that happens to want it.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const get = (id) => {
|
|
69
|
+
const found = byId.get(id);
|
|
70
|
+
if (found === void 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Integration "${id}" is not registered. Registered under "${prefix}": ${[...byId.keys()].join(", ") || "nothing"}.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return found;
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
get,
|
|
79
|
+
has: (id) => byId.has(id),
|
|
80
|
+
list: () => [...byId.values()],
|
|
81
|
+
prefix,
|
|
82
|
+
required: () => {
|
|
83
|
+
if (requiredId === void 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`createIntegrationRegistry("${prefix}") named no \`requiredId\`, so there is no required integration to hand back. Name one, or ask for an id.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return get(requiredId);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/settings.ts
|
|
94
|
+
var OPTIONAL_WRAPPERS = /* @__PURE__ */ new Set(["default", "optional", "prefault"]);
|
|
95
|
+
var WRAPPERS = /* @__PURE__ */ new Set([...OPTIONAL_WRAPPERS, "nullable", "readonly"]);
|
|
96
|
+
var asZod = (schema) => schema === void 0 ? void 0 : schema;
|
|
97
|
+
var unwrap = (field) => {
|
|
98
|
+
let schema = field;
|
|
99
|
+
let required = true;
|
|
100
|
+
let description = field.description;
|
|
101
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
102
|
+
const type = schema.def?.type;
|
|
103
|
+
const inner = schema.def?.innerType;
|
|
104
|
+
if (type === void 0 || inner === void 0 || !WRAPPERS.has(type)) break;
|
|
105
|
+
if (OPTIONAL_WRAPPERS.has(type)) required = false;
|
|
106
|
+
schema = inner;
|
|
107
|
+
description = description ?? schema.description;
|
|
108
|
+
}
|
|
109
|
+
return { description, required, schema };
|
|
110
|
+
};
|
|
111
|
+
var optionsOf = (schema) => {
|
|
112
|
+
const entries = schema.def?.entries;
|
|
113
|
+
if (entries === void 0) return void 0;
|
|
114
|
+
return Object.values(entries).map(String);
|
|
115
|
+
};
|
|
116
|
+
var settingsFieldsOf = (integration) => {
|
|
117
|
+
const schema = asZod(integration.settingsSchema);
|
|
118
|
+
if (schema === void 0) return [];
|
|
119
|
+
if (schema.def === void 0) {
|
|
120
|
+
throw new TypeError(
|
|
121
|
+
`settingsFieldsOf("${integration.id}"): the settings schema is not one this reader can see into. It parses, so it is a schema \u2014 but its fields are not where zod 4 keeps them, and zod 3 spells its shape as a function behind \`_def\`. Pass a zod 4 schema (the declared peer is ^4.0.0), or build the fields by hand for this one.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const shape = schema.def.shape;
|
|
125
|
+
if (shape === void 0) return [];
|
|
126
|
+
return Object.entries(shape).map(([name, field]) => {
|
|
127
|
+
const { description, required, schema: inner } = unwrap(field);
|
|
128
|
+
return {
|
|
129
|
+
description,
|
|
130
|
+
name,
|
|
131
|
+
options: optionsOf(inner),
|
|
132
|
+
required,
|
|
133
|
+
type: inner.def?.type ?? "unknown"
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
export {
|
|
138
|
+
createIntegrationRegistry,
|
|
139
|
+
defineIntegration,
|
|
140
|
+
isIntegration,
|
|
141
|
+
settingsFieldsOf
|
|
142
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geonosis/integrations",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "One integration seam: a prefixed id, an optional settings schema, one registry that refuses a duplicate, a wrong prefix or a missing required id — and settings fields derived from the schema rather than hand-mapped.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"integrations",
|
|
7
|
+
"plugin",
|
|
8
|
+
"registry",
|
|
9
|
+
"provider",
|
|
10
|
+
"zod",
|
|
11
|
+
"settings"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/microcompanies/geonosis/tree/main/packages/integrations",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/microcompanies/geonosis.git",
|
|
17
|
+
"directory": "packages/integrations"
|
|
18
|
+
},
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"zod": "^4.0.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"zod": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"zod": "^4.0.0",
|
|
38
|
+
"zod-4-2": "npm:zod@4.2.0",
|
|
39
|
+
"zod-3": "npm:zod@3.25.76"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|