@comms-id/asx 0.4.0 → 0.4.2

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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/browser-profile.d.ts +21 -0
  3. package/dist/browser-profile.js +115 -0
  4. package/dist/client-replies.d.ts +178 -0
  5. package/dist/client-replies.js +57 -0
  6. package/dist/client.d.ts +25 -0
  7. package/dist/client.js +179 -0
  8. package/dist/collection.d.ts +555 -0
  9. package/dist/collection.js +194 -0
  10. package/dist/company-events.d.ts +2 -0
  11. package/dist/company-events.js +48 -0
  12. package/dist/company-record.d.ts +4 -0
  13. package/dist/company-record.js +60 -0
  14. package/dist/company-stream-client.d.ts +4 -0
  15. package/dist/company-stream-client.js +139 -0
  16. package/dist/company-stream.d.ts +46 -0
  17. package/dist/company-stream.js +117 -0
  18. package/dist/contract/http-api.d.ts +139 -0
  19. package/dist/contract/http-api.js +100 -0
  20. package/dist/directory-search.d.ts +2 -0
  21. package/dist/directory-search.js +33 -0
  22. package/dist/enrichment.d.ts +75 -0
  23. package/dist/enrichment.js +114 -0
  24. package/dist/http.d.ts +1 -0
  25. package/dist/http.js +49 -0
  26. package/dist/live.d.ts +56 -0
  27. package/dist/live.js +129 -0
  28. package/dist/model.d.ts +112 -0
  29. package/dist/model.js +86 -0
  30. package/dist/parse.d.ts +4 -0
  31. package/dist/parse.js +130 -0
  32. package/dist/profile-contact.d.ts +13 -0
  33. package/dist/profile-contact.js +66 -0
  34. package/dist/publication.d.ts +25 -0
  35. package/dist/publication.js +89 -0
  36. package/dist/refresh-coordinator.d.ts +96 -0
  37. package/dist/refresh-coordinator.js +194 -0
  38. package/dist/refresh.d.ts +126 -0
  39. package/dist/refresh.js +157 -0
  40. package/dist/requests.d.ts +10 -0
  41. package/dist/requests.js +21 -0
  42. package/dist/runtime/http.js +8 -1
  43. package/dist/search-service.d.ts +16 -0
  44. package/dist/search-service.js +64 -0
  45. package/dist/service.d.ts +35 -0
  46. package/dist/service.js +99 -0
  47. package/dist/snapshot.d.ts +328 -0
  48. package/dist/snapshot.js +151 -0
  49. package/dist/source-record.d.ts +63 -0
  50. package/dist/source-record.js +91 -0
  51. package/package.json +3 -3
@@ -0,0 +1,139 @@
1
+ import { Schema } from "effect";
2
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi";
3
+ export declare const ASX_API_MAJOR = 1;
4
+ export declare const ASX_API_PREFIX = "/asx/v1";
5
+ export declare const AsxApi: HttpApi.HttpApi<"asx", HttpApiGroup.HttpApiGroup<"asx", HttpApiEndpoint.HttpApiEndpoint<"lookup", "POST", "/asx/v1/lookup", never, never, Schema.toCodecJson<Schema.Struct<{
6
+ readonly ticker: Schema.String;
7
+ readonly mode: Schema.optionalKey<Schema.Literals<readonly ["live", "snapshot", "live-first"]>>;
8
+ }>>, never, Schema.toCodecJson<Schema.Union<readonly [Schema.Struct<{
9
+ readonly status: Schema.Literal<"found">;
10
+ readonly source: Schema.Literal<"live">;
11
+ readonly observation: Schema.Struct<{
12
+ readonly company: Schema.Struct<{
13
+ readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
14
+ readonly ticker: Schema.String;
15
+ readonly sourceId: Schema.String;
16
+ readonly companyName: Schema.String;
17
+ readonly address: Schema.NullOr<Schema.String>;
18
+ readonly tel: Schema.NullOr<Schema.String>;
19
+ readonly fax: Schema.NullOr<Schema.String>;
20
+ readonly website: Schema.NullOr<Schema.String>;
21
+ readonly description: Schema.NullOr<Schema.String>;
22
+ readonly issueType: Schema.NullOr<Schema.String>;
23
+ readonly foreignExempt: Schema.NullOr<Schema.Boolean>;
24
+ readonly people: Schema.$Array<Schema.Struct<{
25
+ readonly sourceName: Schema.String;
26
+ readonly displayName: Schema.String;
27
+ readonly title: Schema.String;
28
+ readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
29
+ }>>;
30
+ }>;
31
+ readonly startedAt: Schema.String;
32
+ readonly observedAt: Schema.String;
33
+ readonly lastChangedAt: Schema.String;
34
+ }>;
35
+ }>, Schema.Struct<{
36
+ readonly status: Schema.Literal<"missing">;
37
+ readonly source: Schema.Literal<"live">;
38
+ readonly startedAt: Schema.String;
39
+ readonly observedAt: Schema.String;
40
+ }>, Schema.Struct<{
41
+ readonly status: Schema.Literal<"found">;
42
+ readonly source: Schema.Literal<"snapshot">;
43
+ readonly observation: Schema.Struct<{
44
+ readonly company: Schema.Struct<{
45
+ readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
46
+ readonly ticker: Schema.String;
47
+ readonly sourceId: Schema.String;
48
+ readonly companyName: Schema.String;
49
+ readonly address: Schema.NullOr<Schema.String>;
50
+ readonly tel: Schema.NullOr<Schema.String>;
51
+ readonly fax: Schema.NullOr<Schema.String>;
52
+ readonly website: Schema.NullOr<Schema.String>;
53
+ readonly description: Schema.NullOr<Schema.String>;
54
+ readonly issueType: Schema.NullOr<Schema.String>;
55
+ readonly foreignExempt: Schema.NullOr<Schema.Boolean>;
56
+ readonly people: Schema.$Array<Schema.Struct<{
57
+ readonly sourceName: Schema.String;
58
+ readonly displayName: Schema.String;
59
+ readonly title: Schema.String;
60
+ readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
61
+ }>>;
62
+ }>;
63
+ readonly startedAt: Schema.String;
64
+ readonly observedAt: Schema.String;
65
+ readonly lastChangedAt: Schema.String;
66
+ }>;
67
+ readonly generation: Schema.String;
68
+ readonly enrichment: Schema.optionalKey<Schema.Struct<{
69
+ readonly ticker: Schema.String;
70
+ readonly sourceId: Schema.NonEmptyString;
71
+ readonly abn: Schema.String;
72
+ readonly acn: Schema.NullOr<Schema.String>;
73
+ readonly legalName: Schema.NonEmptyString;
74
+ readonly identityEvidence: Schema.Struct<{
75
+ readonly url: Schema.String;
76
+ readonly sha256: Schema.String;
77
+ readonly observedAt: Schema.String;
78
+ readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
79
+ }>;
80
+ readonly abrEvidence: Schema.Struct<{
81
+ readonly source: Schema.Literals<readonly ["abr-live", "abr-snapshot"]>;
82
+ readonly observedAt: Schema.String;
83
+ }>;
84
+ readonly website: Schema.NullOr<Schema.Struct<{
85
+ readonly url: Schema.String;
86
+ readonly evidence: Schema.Struct<{
87
+ readonly url: Schema.String;
88
+ readonly sha256: Schema.String;
89
+ readonly observedAt: Schema.String;
90
+ readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
91
+ }>;
92
+ }>>;
93
+ }>>;
94
+ readonly requiresRecheck: Schema.Literal<true>;
95
+ readonly fallbackReason: Schema.optionalKey<Schema.Literals<readonly ["INVALID_INPUT", "PARSE_FAILED", "SOURCE_MISMATCH", "TRANSPORT_FAILED", "TIMEOUT", "RATE_LIMITED", "UPSTREAM_UNAVAILABLE", "CONFIGURATION_ERROR", "SNAPSHOT_UNAVAILABLE", "CANCELLED", "INTERNAL_ERROR", "ACCESS_DENIED", "UPSTREAM_REJECTED", "PERSISTENCE_FAILED", "PUBLICATION_CONFLICT", "COVERAGE_REJECTED"]>>;
96
+ }>, Schema.Struct<{
97
+ readonly status: Schema.Literal<"missing">;
98
+ readonly source: Schema.Literal<"snapshot">;
99
+ readonly generation: Schema.String;
100
+ }>]>>, Schema.toCodecJson<Schema.Struct<{
101
+ readonly code: Schema.Literals<readonly [string, ...string[]]>;
102
+ readonly message: Schema.String;
103
+ readonly retryable: Schema.Boolean;
104
+ readonly requestId: Schema.String;
105
+ }>>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"suggest", "POST", "/asx/v1/suggest", never, never, Schema.toCodecJson<Schema.Struct<{
106
+ readonly mode: Schema.optionalKey<Schema.Literals<readonly ["live", "snapshot", "live-first"]>>;
107
+ readonly query: Schema.String;
108
+ readonly limit: Schema.optionalKey<Schema.Number>;
109
+ }>>, never, Schema.toCodecJson<Schema.Union<readonly [Schema.Struct<{
110
+ readonly source: Schema.Literal<"live">;
111
+ readonly directoryObservedAt: Schema.String;
112
+ readonly suggestions: Schema.$Array<Schema.Struct<{
113
+ readonly ticker: Schema.String;
114
+ readonly companyName: Schema.String;
115
+ readonly industryGroup: Schema.NullOr<Schema.String>;
116
+ readonly listingDate: Schema.NullOr<Schema.String>;
117
+ readonly marketCapitalisation: Schema.NullOr<Schema.Number>;
118
+ }>>;
119
+ }>, Schema.Struct<{
120
+ readonly source: Schema.Literal<"snapshot">;
121
+ readonly generation: Schema.String;
122
+ readonly directoryObservedAt: Schema.String;
123
+ readonly requiresRecheck: Schema.Literal<true>;
124
+ readonly suggestions: Schema.$Array<Schema.Struct<{
125
+ readonly ticker: Schema.String;
126
+ readonly companyName: Schema.String;
127
+ readonly industryGroup: Schema.NullOr<Schema.String>;
128
+ readonly listingDate: Schema.NullOr<Schema.String>;
129
+ readonly marketCapitalisation: Schema.NullOr<Schema.Number>;
130
+ }>>;
131
+ readonly fallbackReason: Schema.optionalKey<Schema.Literals<readonly ["INVALID_INPUT", "PARSE_FAILED", "SOURCE_MISMATCH", "TRANSPORT_FAILED", "TIMEOUT", "RATE_LIMITED", "UPSTREAM_UNAVAILABLE", "CONFIGURATION_ERROR", "SNAPSHOT_UNAVAILABLE", "CANCELLED", "INTERNAL_ERROR", "ACCESS_DENIED", "UPSTREAM_REJECTED", "PERSISTENCE_FAILED", "PUBLICATION_CONFLICT", "COVERAGE_REJECTED"]>>;
132
+ }>]>>, Schema.toCodecJson<Schema.Struct<{
133
+ readonly code: Schema.Literals<readonly [string, ...string[]]>;
134
+ readonly message: Schema.String;
135
+ readonly retryable: Schema.Boolean;
136
+ readonly requestId: Schema.String;
137
+ }>>, never, never>, false>>;
138
+ /** The OpenAPI document for the ASX contract. */
139
+ export declare const asxOpenApiDocument: () => OpenApi.OpenAPISpec;
@@ -0,0 +1,100 @@
1
+ import { Schema } from "effect";
2
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi, } from "effect/unstable/httpapi";
3
+ import { LookupResponse, SuggestionsResponse } from "../client-replies.js";
4
+ import { AsxLookupRequestSchema, AsxSuggestRequestSchema } from "../requests.js";
5
+ // The ASX HTTP contract (ADR-0052, platform #515 wave 3, T2), built from the package's own
6
+ // request and reply schemas.
7
+ const errorBody = (name, codes, status, description) => Schema.Struct({
8
+ code: Schema.Literals(codes),
9
+ message: Schema.String,
10
+ retryable: Schema.Boolean,
11
+ requestId: Schema.String.annotate({ description: "The request's id, for support." }),
12
+ })
13
+ .annotate({ identifier: `AsxError_${name}`, description })
14
+ .pipe(HttpApiSchema.status(status));
15
+ const ERRORS = [
16
+ errorBody("INVALID_INPUT", ["INVALID_INPUT"], 400, "The body is not valid for this operation."),
17
+ errorBody("UNAUTHORIZED", ["UNAUTHORIZED"], 401, "The request carries no identified caller (ADR-0018)."),
18
+ errorBody("VERSION_RETIRED", ["VERSION_RETIRED"], 410, "This major version is retired (ADR-0050)."),
19
+ errorBody("FAIR_USE_CEILING", ["FAIR_USE_CEILING"], 429, "The fair-use ceiling is reached; retry after Retry-After."),
20
+ errorBody("UPSTREAM", ["UPSTREAM_REJECTED", "PARSE_FAILED", "SOURCE_MISMATCH"], 502, "ASX answered, but its answer could not be used."),
21
+ errorBody("UNAVAILABLE", [
22
+ "TRANSPORT_FAILED",
23
+ "RATE_LIMITED",
24
+ "UPSTREAM_UNAVAILABLE",
25
+ "ACCESS_DENIED",
26
+ "CONFIGURATION_ERROR",
27
+ "SNAPSHOT_UNAVAILABLE",
28
+ "PERSISTENCE_FAILED",
29
+ ], 503, "The source is unavailable."),
30
+ errorBody("TIMEOUT", ["TIMEOUT"], 504, "The operation did not finish within its deadline."),
31
+ ];
32
+ export const ASX_API_MAJOR = 1;
33
+ export const ASX_API_PREFIX = `/asx/v${ASX_API_MAJOR}`;
34
+ const naming = (method) => ({
35
+ "x-fern-sdk-group-name": "asx",
36
+ "x-fern-sdk-method-name": method,
37
+ "x-fern-availability": "beta",
38
+ "x-comms-id-safe-to-retry": true,
39
+ "x-comms-id-auth": { audience: "asx-lookup", scope: "asx" },
40
+ });
41
+ const LOOKUP_EXAMPLE = Schema.decodeUnknownSync(LookupResponse)({
42
+ status: "missing",
43
+ source: "live",
44
+ startedAt: "2026-10-03T00:00:00.000Z",
45
+ observedAt: "2026-10-03T00:00:01.000Z",
46
+ });
47
+ const SUGGEST_EXAMPLE = Schema.decodeUnknownSync(SuggestionsResponse)({
48
+ source: "live",
49
+ directoryObservedAt: "2026-10-03T00:00:00.000Z",
50
+ suggestions: [],
51
+ });
52
+ // Request examples, decoded once through their schemas so an invalid one fails at load.
53
+ const exampleOf = (schema, value) => {
54
+ Schema.decodeUnknownSync(schema)(value);
55
+ return value;
56
+ };
57
+ const REQUEST_EXAMPLES = {
58
+ lookup: exampleOf(AsxLookupRequestSchema, { ticker: "BHP" }),
59
+ suggest: exampleOf(AsxSuggestRequestSchema, { query: "BH", limit: 5 }),
60
+ };
61
+ export const AsxApi = HttpApi.make("asx")
62
+ .add(HttpApiGroup.make("asx")
63
+ .add(HttpApiEndpoint.post("lookup", `${ASX_API_PREFIX}/lookup`, {
64
+ payload: AsxLookupRequestSchema,
65
+ success: LookupResponse.annotate({ examples: [LOOKUP_EXAMPLE] }),
66
+ error: ERRORS,
67
+ })
68
+ .annotate(OpenApi.Description, "Look up an ASX-listed company by ticker, live or from the stored directory.")
69
+ .annotate(OpenApi.Override, naming("lookup")))
70
+ .add(HttpApiEndpoint.post("suggest", `${ASX_API_PREFIX}/suggest`, {
71
+ payload: AsxSuggestRequestSchema,
72
+ success: SuggestionsResponse.annotate({ examples: [SUGGEST_EXAMPLE] }),
73
+ error: ERRORS,
74
+ })
75
+ .annotate(OpenApi.Description, "Suggest ASX-listed companies for a ticker or name prefix, at most ten.")
76
+ .annotate(OpenApi.Override, naming("suggest"))))
77
+ .annotate(OpenApi.Title, "Comms.ID ASX")
78
+ .annotate(OpenApi.Version, `${ASX_API_MAJOR}.0.0`)
79
+ // The Sydney front door (TLS terminates in Australia). Its pool routes /asx/* here and
80
+ // strips the prefix; the worker accepts /asx/v1/* and /v1/* alike.
81
+ .annotate(OpenApi.Servers, [
82
+ {
83
+ url: "https://o5tejx16gc.execute-api.ap-southeast-2.amazonaws.com",
84
+ description: "Sydney front door (TLS terminates in Australia)",
85
+ },
86
+ ])
87
+ // The request examples go in the named map of each request media type (the convention of
88
+ // every Comms.ID contract).
89
+ .annotate(OpenApi.Transform, (spec) => {
90
+ const { paths } = spec;
91
+ for (const [operation, value] of Object.entries(REQUEST_EXAMPLES)) {
92
+ const media = paths[`${ASX_API_PREFIX}/${operation}`]?.post?.requestBody?.content["application/json"];
93
+ if (media !== undefined) {
94
+ media.examples = { default: { value } };
95
+ }
96
+ }
97
+ return spec;
98
+ });
99
+ /** The OpenAPI document for the ASX contract. */
100
+ export const asxOpenApiDocument = () => OpenApi.fromApi(AsxApi);
@@ -0,0 +1,2 @@
1
+ import { type DirectoryEntry } from "./model.js";
2
+ export declare function suggestDirectory(entries: readonly DirectoryEntry[], query: string, limit: number): readonly DirectoryEntry[];
@@ -0,0 +1,33 @@
1
+ import { failure } from "./model.js";
2
+ export function suggestDirectory(entries, query, limit) {
3
+ if (typeof query !== "string" ||
4
+ query.length > 200 ||
5
+ !Number.isInteger(limit) ||
6
+ limit < 1 ||
7
+ limit > 10)
8
+ throw failure("INVALID_INPUT");
9
+ const normalized = query.trim().toUpperCase();
10
+ if (normalized.length < 2)
11
+ return [];
12
+ const tokens = normalized.split(/\s+/);
13
+ return entries
14
+ .map((directory) => {
15
+ const name = directory.companyName.toUpperCase();
16
+ const score = directory.ticker === normalized
17
+ ? 0
18
+ : directory.ticker.startsWith(normalized)
19
+ ? 1
20
+ : name.startsWith(normalized)
21
+ ? 2
22
+ : tokens.every((token) => name.includes(token))
23
+ ? 3
24
+ : 4;
25
+ return { directory, score };
26
+ })
27
+ .filter(({ score }) => score < 4)
28
+ .sort((a, b) => a.score - b.score ||
29
+ a.directory.companyName.localeCompare(b.directory.companyName, "en") ||
30
+ a.directory.ticker.localeCompare(b.directory.ticker, "en"))
31
+ .slice(0, limit)
32
+ .map(({ directory }) => structuredClone(directory));
33
+ }
@@ -0,0 +1,75 @@
1
+ import { Schema } from "effect";
2
+ import { Company } from "./model.js";
3
+ export declare function validAbn(abn: string): boolean;
4
+ export declare const Evidence: Schema.Struct<{
5
+ readonly url: Schema.String;
6
+ readonly sha256: Schema.String;
7
+ readonly observedAt: Schema.String;
8
+ readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
9
+ }>;
10
+ export interface Evidence extends Schema.Schema.Type<typeof Evidence> {
11
+ }
12
+ export declare const ConfirmedEnrichment: Schema.Struct<{
13
+ readonly ticker: Schema.String;
14
+ readonly sourceId: Schema.NonEmptyString;
15
+ readonly abn: Schema.String;
16
+ readonly acn: Schema.NullOr<Schema.String>;
17
+ readonly legalName: Schema.NonEmptyString;
18
+ readonly identityEvidence: Schema.Struct<{
19
+ readonly url: Schema.String;
20
+ readonly sha256: Schema.String;
21
+ readonly observedAt: Schema.String;
22
+ readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
23
+ }>;
24
+ readonly abrEvidence: Schema.Struct<{
25
+ readonly source: Schema.Literals<readonly ["abr-live", "abr-snapshot"]>;
26
+ readonly observedAt: Schema.String;
27
+ }>;
28
+ readonly website: Schema.NullOr<Schema.Struct<{
29
+ readonly url: Schema.String;
30
+ readonly evidence: Schema.Struct<{
31
+ readonly url: Schema.String;
32
+ readonly sha256: Schema.String;
33
+ readonly observedAt: Schema.String;
34
+ readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
35
+ }>;
36
+ }>>;
37
+ }>;
38
+ export interface ConfirmedEnrichment extends Schema.Schema.Type<typeof ConfirmedEnrichment> {
39
+ }
40
+ export declare const EnrichmentResult: Schema.Union<readonly [Schema.Struct<{
41
+ readonly status: Schema.Literal<"confirmed">;
42
+ readonly enrichment: Schema.Struct<{
43
+ readonly ticker: Schema.String;
44
+ readonly sourceId: Schema.NonEmptyString;
45
+ readonly abn: Schema.String;
46
+ readonly acn: Schema.NullOr<Schema.String>;
47
+ readonly legalName: Schema.NonEmptyString;
48
+ readonly identityEvidence: Schema.Struct<{
49
+ readonly url: Schema.String;
50
+ readonly sha256: Schema.String;
51
+ readonly observedAt: Schema.String;
52
+ readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
53
+ }>;
54
+ readonly abrEvidence: Schema.Struct<{
55
+ readonly source: Schema.Literals<readonly ["abr-live", "abr-snapshot"]>;
56
+ readonly observedAt: Schema.String;
57
+ }>;
58
+ readonly website: Schema.NullOr<Schema.Struct<{
59
+ readonly url: Schema.String;
60
+ readonly evidence: Schema.Struct<{
61
+ readonly url: Schema.String;
62
+ readonly sha256: Schema.String;
63
+ readonly observedAt: Schema.String;
64
+ readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
65
+ }>;
66
+ }>>;
67
+ }>;
68
+ }>, Schema.Struct<{
69
+ readonly status: Schema.Literal<"unresolved">;
70
+ readonly reason: Schema.Literals<readonly ["NO_EVIDENCE", "AMBIGUOUS", "INVALID_EVIDENCE", "IDENTITY_MISMATCH"]>;
71
+ }>]>;
72
+ export type EnrichmentResult = typeof EnrichmentResult.Type;
73
+ /** Inputs are accepted primary-source observations from trusted collectors, never public request data. */
74
+ export declare function confirmEnrichment(companyInput: unknown, issuerCandidates: unknown, abrInput: unknown): EnrichmentResult;
75
+ export declare function validEnrichment(value: ConfirmedEnrichment, company: Company): boolean;
@@ -0,0 +1,114 @@
1
+ import { Result, Schema } from "effect";
2
+ import { Company, isTimestamp, Timestamp, Ticker } from "./model.js";
3
+ // ABR's published modulus-89 rule; checksum validity alone never proves issuer identity.
4
+ export function validAbn(abn) {
5
+ if (!/^\d{11}$/.test(abn))
6
+ return false;
7
+ const weights = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
8
+ return (Array.from(abn).reduce((sum, digit, index) => sum + (Number(digit) - (index === 0 ? 1 : 0)) * weights[index], 0) %
9
+ 89 ===
10
+ 0);
11
+ }
12
+ const ABN = Schema.String.check(Schema.makeFilter(validAbn));
13
+ const ACN = Schema.NullOr(Schema.String.check(Schema.isPattern(/^\d{9}$/)));
14
+ const HttpsUrl = Schema.String.check(Schema.makeFilter((value) => {
15
+ try {
16
+ const url = new URL(value);
17
+ return (url.protocol === "https:" &&
18
+ !url.username &&
19
+ !url.password &&
20
+ !!url.hostname &&
21
+ value.length <= 2048);
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }));
27
+ export const Evidence = Schema.Struct({
28
+ url: HttpsUrl,
29
+ sha256: Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/)),
30
+ observedAt: Timestamp,
31
+ kind: Schema.Literals(["issuer-filing", "issuer-website"]),
32
+ });
33
+ export const ConfirmedEnrichment = Schema.Struct({
34
+ ticker: Ticker,
35
+ sourceId: Schema.NonEmptyString,
36
+ abn: ABN,
37
+ acn: ACN,
38
+ legalName: Schema.NonEmptyString,
39
+ identityEvidence: Evidence,
40
+ abrEvidence: Schema.Struct({
41
+ source: Schema.Literals(["abr-live", "abr-snapshot"]),
42
+ observedAt: Timestamp,
43
+ }),
44
+ website: Schema.NullOr(Schema.Struct({ url: HttpsUrl, evidence: Evidence })),
45
+ });
46
+ const IssuerEvidence = Schema.Struct({
47
+ ticker: Ticker,
48
+ sourceId: Schema.NonEmptyString,
49
+ issuerName: Schema.NonEmptyString,
50
+ abn: ABN,
51
+ acn: ACN,
52
+ evidence: Evidence,
53
+ website: Schema.NullOr(Schema.Struct({ url: HttpsUrl, evidence: Evidence })),
54
+ });
55
+ const AbrEvidence = Schema.Struct({
56
+ abn: ABN,
57
+ acn: ACN,
58
+ legalName: Schema.NonEmptyString,
59
+ source: Schema.Literals(["abr-live", "abr-snapshot"]),
60
+ observedAt: Timestamp,
61
+ });
62
+ export const EnrichmentResult = Schema.Union([
63
+ Schema.Struct({ status: Schema.Literal("confirmed"), enrichment: ConfirmedEnrichment }),
64
+ Schema.Struct({
65
+ status: Schema.Literal("unresolved"),
66
+ reason: Schema.Literals(["NO_EVIDENCE", "AMBIGUOUS", "INVALID_EVIDENCE", "IDENTITY_MISMATCH"]),
67
+ }),
68
+ ]);
69
+ const nameKey = (value) => value.trim().toUpperCase().replace(/\s+/g, " ");
70
+ /** Inputs are accepted primary-source observations from trusted collectors, never public request data. */
71
+ export function confirmEnrichment(companyInput, issuerCandidates, abrInput) {
72
+ const company = Schema.decodeUnknownResult(Company)(companyInput);
73
+ const candidates = Schema.decodeUnknownResult(Schema.Array(IssuerEvidence))(issuerCandidates);
74
+ const abr = Schema.decodeUnknownResult(AbrEvidence)(abrInput);
75
+ if (Result.isFailure(company) || Result.isFailure(candidates) || Result.isFailure(abr))
76
+ return { status: "unresolved", reason: "INVALID_EVIDENCE" };
77
+ if (candidates.success.length === 0)
78
+ return { status: "unresolved", reason: "NO_EVIDENCE" };
79
+ if (candidates.success.length !== 1)
80
+ return { status: "unresolved", reason: "AMBIGUOUS" };
81
+ const issuer = candidates.success[0];
82
+ if (!isTimestamp(issuer.evidence.observedAt) ||
83
+ !isTimestamp(abr.success.observedAt) ||
84
+ (issuer.website && !isTimestamp(issuer.website.evidence.observedAt)))
85
+ return { status: "unresolved", reason: "INVALID_EVIDENCE" };
86
+ if (issuer.sourceId !== company.success.sourceId ||
87
+ issuer.ticker !== company.success.ticker ||
88
+ nameKey(issuer.issuerName) !== nameKey(company.success.companyName) ||
89
+ nameKey(issuer.issuerName) !== nameKey(abr.success.legalName) ||
90
+ issuer.abn !== abr.success.abn ||
91
+ (issuer.acn !== null && issuer.acn !== abr.success.acn))
92
+ return { status: "unresolved", reason: "IDENTITY_MISMATCH" };
93
+ return {
94
+ status: "confirmed",
95
+ enrichment: {
96
+ ticker: issuer.ticker,
97
+ sourceId: issuer.sourceId,
98
+ abn: issuer.abn,
99
+ acn: abr.success.acn,
100
+ legalName: abr.success.legalName,
101
+ identityEvidence: issuer.evidence,
102
+ abrEvidence: { source: abr.success.source, observedAt: abr.success.observedAt },
103
+ website: issuer.website,
104
+ },
105
+ };
106
+ }
107
+ export function validEnrichment(value, company) {
108
+ return (value.sourceId === company.sourceId &&
109
+ value.ticker === company.ticker &&
110
+ nameKey(value.legalName) === nameKey(company.companyName) &&
111
+ isTimestamp(value.identityEvidence.observedAt) &&
112
+ isTimestamp(value.abrEvidence.observedAt) &&
113
+ (value.website === null || isTimestamp(value.website.evidence.observedAt)));
114
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function boundedText(response: Response, limit: number, signal: AbortSignal): Promise<string>;
package/dist/http.js ADDED
@@ -0,0 +1,49 @@
1
+ import { failure } from "./model.js";
2
+ export async function boundedText(response, limit, signal) {
3
+ if (!response.body)
4
+ throw failure("PARSE_FAILED");
5
+ const reader = response.body.getReader();
6
+ let length = 0;
7
+ let body = "";
8
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
9
+ const decode = (bytes) => {
10
+ try {
11
+ return decoder.decode(bytes, { stream: bytes !== undefined });
12
+ }
13
+ catch {
14
+ throw failure("PARSE_FAILED");
15
+ }
16
+ };
17
+ const abort = () => {
18
+ void reader.cancel().catch(() => undefined);
19
+ };
20
+ signal.addEventListener("abort", abort, { once: true });
21
+ try {
22
+ while (true) {
23
+ if (signal.aborted)
24
+ throw failure("CANCELLED");
25
+ const chunk = await reader.read();
26
+ if (chunk.done)
27
+ break;
28
+ const bytes = chunk.value;
29
+ if (!(bytes instanceof Uint8Array))
30
+ throw failure("PARSE_FAILED");
31
+ length += bytes.byteLength;
32
+ if (length > limit)
33
+ throw failure("PARSE_FAILED");
34
+ body += decode(bytes);
35
+ }
36
+ if (signal.aborted)
37
+ throw failure("CANCELLED");
38
+ return body + decode();
39
+ }
40
+ finally {
41
+ signal.removeEventListener("abort", abort);
42
+ try {
43
+ await reader.cancel();
44
+ }
45
+ finally {
46
+ reader.releaseLock();
47
+ }
48
+ }
49
+ }
package/dist/live.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { Schema } from "effect";
2
+ import { type DirectoryEntry } from "./model.js";
3
+ export interface AsxLiveConfig {
4
+ /** Host transport owns network routing; must honour AbortSignal. */
5
+ readonly fetch?: typeof globalThis.fetch;
6
+ readonly timeoutMs?: number;
7
+ }
8
+ /** No persistence, source composition, background work or network call at construction. */
9
+ export declare function createAsxLive(config?: AsxLiveConfig): {
10
+ source: (input: string, signal?: AbortSignal) => Promise<import("./source-record.js").SourceObservation>;
11
+ lookup: (ticker: string, signal?: AbortSignal) => Promise<{
12
+ readonly status: "found";
13
+ readonly observation: {
14
+ readonly company: {
15
+ readonly raw?: {
16
+ readonly [x: string]: Schema.Json;
17
+ } | undefined;
18
+ readonly ticker: string;
19
+ readonly sourceId: string;
20
+ readonly companyName: string;
21
+ readonly address: string | null;
22
+ readonly tel: string | null;
23
+ readonly fax: string | null;
24
+ readonly website: string | null;
25
+ readonly description: string | null;
26
+ readonly issueType: string | null;
27
+ readonly foreignExempt: boolean | null;
28
+ readonly people: readonly {
29
+ readonly sourceName: string;
30
+ readonly displayName: string;
31
+ readonly title: string;
32
+ readonly sourceCategory: "directors" | "secretaries";
33
+ }[];
34
+ };
35
+ readonly startedAt: string;
36
+ readonly observedAt: string;
37
+ readonly lastChangedAt: string;
38
+ };
39
+ } | {
40
+ readonly status: "missing";
41
+ readonly startedAt: string;
42
+ readonly observedAt: string;
43
+ readonly raw?: {
44
+ readonly [x: string]: Schema.Json;
45
+ } | undefined;
46
+ }>;
47
+ directory: (signal?: AbortSignal) => Promise<{
48
+ entries: readonly DirectoryEntry[];
49
+ observedAt: string;
50
+ }>;
51
+ suggest: (query: string, limit?: number, signal?: AbortSignal) => Promise<{
52
+ source: "live";
53
+ directoryObservedAt: string;
54
+ suggestions: readonly DirectoryEntry[];
55
+ }>;
56
+ };